@belte/belte 0.19.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 (326) hide show
  1. package/CHANGELOG.md +313 -0
  2. package/LICENSE +21 -0
  3. package/README.md +559 -0
  4. package/bin/belte.ts +183 -0
  5. package/package.json +110 -0
  6. package/src/App.svelte +31 -0
  7. package/src/appEntry.ts +151 -0
  8. package/src/assets/app.html +14 -0
  9. package/src/belteResolverPlugin.ts +858 -0
  10. package/src/build.ts +147 -0
  11. package/src/buildCli.ts +129 -0
  12. package/src/buildDisconnected.ts +122 -0
  13. package/src/bundleApp.ts +149 -0
  14. package/src/bundleDisconnectedEntry.ts +17 -0
  15. package/src/cliEntry.ts +25 -0
  16. package/src/clientBuildPlugins.ts +41 -0
  17. package/src/clientEntry.ts +7 -0
  18. package/src/compile.ts +64 -0
  19. package/src/controlServerWorker.ts +422 -0
  20. package/src/dedupeSveltePlugin.ts +66 -0
  21. package/src/devEntry.ts +169 -0
  22. package/src/discoveryEntry.ts +81 -0
  23. package/src/lib/browser/applyStreamedResolution.ts +33 -0
  24. package/src/lib/browser/cacheEntryFromSnapshot.ts +48 -0
  25. package/src/lib/browser/flushUnresolvedPlaceholders.ts +16 -0
  26. package/src/lib/browser/installStreamingPlaceholders.ts +32 -0
  27. package/src/lib/browser/openResolveStream.ts +42 -0
  28. package/src/lib/browser/page.svelte.ts +258 -0
  29. package/src/lib/browser/pageStreamController.ts +17 -0
  30. package/src/lib/browser/refetchPlaceholder.ts +12 -0
  31. package/src/lib/browser/remoteProxy.ts +37 -0
  32. package/src/lib/browser/socketChannel.ts +192 -0
  33. package/src/lib/browser/socketProxy.ts +57 -0
  34. package/src/lib/browser/startClient.ts +153 -0
  35. package/src/lib/browser/subscribe.ts +131 -0
  36. package/src/lib/browser/types/Errors.ts +9 -0
  37. package/src/lib/browser/types/Layouts.ts +7 -0
  38. package/src/lib/browser/types/Pages.ts +7 -0
  39. package/src/lib/browser/types/StreamingDeferred.ts +9 -0
  40. package/src/lib/bundle/BundleMenu.ts +11 -0
  41. package/src/lib/bundle/BundleMenuItem.ts +24 -0
  42. package/src/lib/bundle/BundleWindow.ts +36 -0
  43. package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
  44. package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
  45. package/src/lib/bundle/bindConnectedFlag.ts +29 -0
  46. package/src/lib/bundle/bindRequestNavigate.ts +31 -0
  47. package/src/lib/bundle/buildWebviewLib.ts +111 -0
  48. package/src/lib/bundle/disconnected.css +9 -0
  49. package/src/lib/bundle/disconnected.svelte +386 -0
  50. package/src/lib/bundle/ensureWebviewLib.ts +20 -0
  51. package/src/lib/bundle/exitWithParent.ts +28 -0
  52. package/src/lib/bundle/infoPlist.ts +46 -0
  53. package/src/lib/bundle/installDownloads.ts +24 -0
  54. package/src/lib/bundle/installMacMenu.ts +39 -0
  55. package/src/lib/bundle/listenLocalControlServer.ts +19 -0
  56. package/src/lib/bundle/native/belteMenu.mm +422 -0
  57. package/src/lib/bundle/native/webview.h +4557 -0
  58. package/src/lib/bundle/onMenu.ts +41 -0
  59. package/src/lib/bundle/openWebview.ts +104 -0
  60. package/src/lib/bundle/pngToIcns.ts +47 -0
  61. package/src/lib/bundle/probeBelteServer.ts +34 -0
  62. package/src/lib/bundle/resolveServerBinary.ts +12 -0
  63. package/src/lib/bundle/resolveWebviewLib.ts +53 -0
  64. package/src/lib/bundle/serverBinaryFilename.ts +8 -0
  65. package/src/lib/bundle/signMacApp.ts +35 -0
  66. package/src/lib/bundle/spawnEmbeddedServer.ts +65 -0
  67. package/src/lib/bundle/stableLocalPort.ts +19 -0
  68. package/src/lib/bundle/waitForServer.ts +23 -0
  69. package/src/lib/bundle/webviewCachePath.ts +23 -0
  70. package/src/lib/bundle/webviewLibName.ts +11 -0
  71. package/src/lib/cli/connectToServer.ts +23 -0
  72. package/src/lib/cli/createClient.ts +170 -0
  73. package/src/lib/cli/dispatchCommand.ts +71 -0
  74. package/src/lib/cli/loadEnvFromBinaryDir.ts +16 -0
  75. package/src/lib/cli/parseArgvForRpc.ts +97 -0
  76. package/src/lib/cli/printHelp.ts +119 -0
  77. package/src/lib/cli/printSessionHelp.ts +27 -0
  78. package/src/lib/cli/printSessionStatus.ts +21 -0
  79. package/src/lib/cli/printTrimmed.ts +8 -0
  80. package/src/lib/cli/printValue.ts +10 -0
  81. package/src/lib/cli/resolveCliTarget.ts +48 -0
  82. package/src/lib/cli/runCli.ts +139 -0
  83. package/src/lib/cli/runSession.ts +105 -0
  84. package/src/lib/cli/startLocalInstance.ts +14 -0
  85. package/src/lib/cli/tokenizeLine.ts +51 -0
  86. package/src/lib/cli/types/CliManifest.ts +9 -0
  87. package/src/lib/cli/types/CliManifestEntry.ts +17 -0
  88. package/src/lib/cli/types/CliTarget.ts +13 -0
  89. package/src/lib/mcp/annotationsForMethod.ts +29 -0
  90. package/src/lib/mcp/createMcpResourceServer.ts +101 -0
  91. package/src/lib/mcp/createMcpServer.ts +42 -0
  92. package/src/lib/mcp/dispatchMcpRequest.ts +146 -0
  93. package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
  94. package/src/lib/mcp/mcpSurface.ts +265 -0
  95. package/src/lib/mcp/toolResultFromResponse.ts +66 -0
  96. package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
  97. package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
  98. package/src/lib/mcp/types/McpResourceContents.ts +10 -0
  99. package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
  100. package/src/lib/mcp/types/McpResourceServer.ts +12 -0
  101. package/src/lib/mcp/types/McpServer.ts +9 -0
  102. package/src/lib/mcp/types/McpServerOptions.ts +16 -0
  103. package/src/lib/server/AppModule.ts +33 -0
  104. package/src/lib/server/DELETE.ts +9 -0
  105. package/src/lib/server/GET.ts +9 -0
  106. package/src/lib/server/HEAD.ts +9 -0
  107. package/src/lib/server/PATCH.ts +9 -0
  108. package/src/lib/server/POST.ts +9 -0
  109. package/src/lib/server/PUT.ts +9 -0
  110. package/src/lib/server/agent.ts +76 -0
  111. package/src/lib/server/appDataDir.ts +15 -0
  112. package/src/lib/server/cli/buildEnvContent.ts +19 -0
  113. package/src/lib/server/cli/createTarGz.ts +76 -0
  114. package/src/lib/server/cli/handleCliDownload.ts +153 -0
  115. package/src/lib/server/cli/handleCliInstall.ts +37 -0
  116. package/src/lib/server/cli/installScript.ts +29 -0
  117. package/src/lib/server/cli/maxSourceMtime.ts +26 -0
  118. package/src/lib/server/cookies.ts +29 -0
  119. package/src/lib/server/env.ts +50 -0
  120. package/src/lib/server/error.ts +70 -0
  121. package/src/lib/server/json.ts +28 -0
  122. package/src/lib/server/jsonl.ts +46 -0
  123. package/src/lib/server/prompts/definePrompt.ts +20 -0
  124. package/src/lib/server/prompts/promptRegistry.ts +9 -0
  125. package/src/lib/server/prompts/registerPrompt.ts +6 -0
  126. package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
  127. package/src/lib/server/prompts/types/Prompt.ts +13 -0
  128. package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
  129. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
  130. package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
  131. package/src/lib/server/redirect.ts +42 -0
  132. package/src/lib/server/request.ts +18 -0
  133. package/src/lib/server/rpc/defineVerb.ts +133 -0
  134. package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
  135. package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
  136. package/src/lib/server/rpc/parseArgs.ts +95 -0
  137. package/src/lib/server/rpc/registerVerb.ts +6 -0
  138. package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
  139. package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
  140. package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
  141. package/src/lib/server/rpc/types/VerbHelper.ts +68 -0
  142. package/src/lib/server/rpc/types/VerbRegistryEntry.ts +29 -0
  143. package/src/lib/server/rpc/unprocessed.ts +14 -0
  144. package/src/lib/server/rpc/verbRegistry.ts +11 -0
  145. package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
  146. package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
  147. package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +29 -0
  148. package/src/lib/server/runtime/acceptsZstd.ts +8 -0
  149. package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
  150. package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
  151. package/src/lib/server/runtime/containsTraversal.ts +37 -0
  152. package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
  153. package/src/lib/server/runtime/createPublicAssetServer.ts +63 -0
  154. package/src/lib/server/runtime/createRouteDispatcher.ts +100 -0
  155. package/src/lib/server/runtime/createServer.ts +692 -0
  156. package/src/lib/server/runtime/devReloadResponse.ts +35 -0
  157. package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
  158. package/src/lib/server/runtime/envSchemaStore.ts +15 -0
  159. package/src/lib/server/runtime/findOpenPort.ts +35 -0
  160. package/src/lib/server/runtime/getActiveServer.ts +6 -0
  161. package/src/lib/server/runtime/globToPathSet.ts +29 -0
  162. package/src/lib/server/runtime/inProcessServer.ts +20 -0
  163. package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
  164. package/src/lib/server/runtime/isCrossOriginUpgrade.ts +19 -0
  165. package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
  166. package/src/lib/server/runtime/logExposedSurfaces.ts +162 -0
  167. package/src/lib/server/runtime/mimeForExtension.ts +20 -0
  168. package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
  169. package/src/lib/server/runtime/parsePort.ts +11 -0
  170. package/src/lib/server/runtime/registryManifests.ts +66 -0
  171. package/src/lib/server/runtime/requestContext.ts +5 -0
  172. package/src/lib/server/runtime/resolveStreamResponse.ts +29 -0
  173. package/src/lib/server/runtime/runWithRequestScope.ts +57 -0
  174. package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
  175. package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
  176. package/src/lib/server/runtime/serverSlot.ts +13 -0
  177. package/src/lib/server/runtime/setActiveServer.ts +6 -0
  178. package/src/lib/server/runtime/snapshotEntryFromCache.ts +81 -0
  179. package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
  180. package/src/lib/server/runtime/streamFromIterator.ts +86 -0
  181. package/src/lib/server/runtime/streamStash.ts +64 -0
  182. package/src/lib/server/runtime/types/Assets.ts +1 -0
  183. package/src/lib/server/runtime/types/RequestStore.ts +27 -0
  184. package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
  185. package/src/lib/server/server.ts +32 -0
  186. package/src/lib/server/socket.ts +31 -0
  187. package/src/lib/server/sockets/createSocketDispatcher.ts +311 -0
  188. package/src/lib/server/sockets/defineSocket.ts +167 -0
  189. package/src/lib/server/sockets/lookupSocket.ts +6 -0
  190. package/src/lib/server/sockets/recentHistory.ts +11 -0
  191. package/src/lib/server/sockets/registerSocket.ts +6 -0
  192. package/src/lib/server/sockets/socketOperations.ts +35 -0
  193. package/src/lib/server/sockets/socketRegistry.ts +9 -0
  194. package/src/lib/server/sockets/types/Socket.ts +21 -0
  195. package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
  196. package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
  197. package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
  198. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +17 -0
  199. package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
  200. package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
  201. package/src/lib/server/sse.ts +53 -0
  202. package/src/lib/shared/BELTE_PACKAGE_NAME.ts +7 -0
  203. package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
  204. package/src/lib/shared/HttpError.ts +19 -0
  205. package/src/lib/shared/RESOLVE_STREAM_PATH.ts +7 -0
  206. package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
  207. package/src/lib/shared/activeCacheStore.ts +20 -0
  208. package/src/lib/shared/appDataDir.ts +34 -0
  209. package/src/lib/shared/belteImportName.ts +44 -0
  210. package/src/lib/shared/browserClientFlags.ts +10 -0
  211. package/src/lib/shared/buildRpcRequest.ts +70 -0
  212. package/src/lib/shared/bundleLayout.ts +36 -0
  213. package/src/lib/shared/bundled.ts +34 -0
  214. package/src/lib/shared/cache.ts +559 -0
  215. package/src/lib/shared/cacheStoreSlot.ts +16 -0
  216. package/src/lib/shared/canonicalJson.ts +63 -0
  217. package/src/lib/shared/carriesBodyArgs.ts +13 -0
  218. package/src/lib/shared/clearLastConnection.ts +7 -0
  219. package/src/lib/shared/commandNameForUrl.ts +17 -0
  220. package/src/lib/shared/createCacheStore.ts +75 -0
  221. package/src/lib/shared/createPushIterator.ts +93 -0
  222. package/src/lib/shared/createRemoteFunction.ts +99 -0
  223. package/src/lib/shared/decodeResponse.ts +47 -0
  224. package/src/lib/shared/detectTarget.ts +27 -0
  225. package/src/lib/shared/exeSuffix.ts +9 -0
  226. package/src/lib/shared/exitOnBuildFailure.ts +17 -0
  227. package/src/lib/shared/extraForwardHeaders.ts +16 -0
  228. package/src/lib/shared/fileStem.ts +9 -0
  229. package/src/lib/shared/findExportCallSite.ts +479 -0
  230. package/src/lib/shared/forwardHeaders.ts +41 -0
  231. package/src/lib/shared/getRemoteMeta.ts +5 -0
  232. package/src/lib/shared/globalCacheStore.ts +15 -0
  233. package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
  234. package/src/lib/shared/importNamesToStrip.ts +13 -0
  235. package/src/lib/shared/invalidateEvent.ts +11 -0
  236. package/src/lib/shared/isCompileTarget.ts +15 -0
  237. package/src/lib/shared/isDebugEnabled.ts +23 -0
  238. package/src/lib/shared/isModuleNotFound.ts +16 -0
  239. package/src/lib/shared/isReadOnlyMethod.ts +14 -0
  240. package/src/lib/shared/isStreamingResponse.ts +11 -0
  241. package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
  242. package/src/lib/shared/jsonSchemaForSchema.ts +32 -0
  243. package/src/lib/shared/jsonlErrorFrame.ts +24 -0
  244. package/src/lib/shared/keyForRemoteCall.ts +29 -0
  245. package/src/lib/shared/lastConnectionPath.ts +7 -0
  246. package/src/lib/shared/loadEnvFile.ts +17 -0
  247. package/src/lib/shared/loadEnvFromDataDir.ts +15 -0
  248. package/src/lib/shared/loadSvelteConfig.ts +18 -0
  249. package/src/lib/shared/log.ts +104 -0
  250. package/src/lib/shared/manifestModule.ts +39 -0
  251. package/src/lib/shared/memoizeByKey.ts +24 -0
  252. package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
  253. package/src/lib/shared/normalizeTarget.ts +10 -0
  254. package/src/lib/shared/pageUrlForFile.ts +14 -0
  255. package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
  256. package/src/lib/shared/parseEnv.ts +30 -0
  257. package/src/lib/shared/parsePromptMarkdown.ts +34 -0
  258. package/src/lib/shared/parseRouteSegments.ts +22 -0
  259. package/src/lib/shared/prepareRpcModule.ts +59 -0
  260. package/src/lib/shared/prepareSocketModule.ts +49 -0
  261. package/src/lib/shared/programNameForPackage.ts +14 -0
  262. package/src/lib/shared/promptNameForFile.ts +10 -0
  263. package/src/lib/shared/queryStringFromArgs.ts +27 -0
  264. package/src/lib/shared/readEnvFile.ts +15 -0
  265. package/src/lib/shared/readLastConnection.ts +18 -0
  266. package/src/lib/shared/readPackageJson.ts +9 -0
  267. package/src/lib/shared/recordRemoteMeta.ts +5 -0
  268. package/src/lib/shared/remoteMetaStore.ts +16 -0
  269. package/src/lib/shared/resolveClientFlags.ts +20 -0
  270. package/src/lib/shared/responseErrorText.ts +9 -0
  271. package/src/lib/shared/rpcUrlForFile.ts +19 -0
  272. package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
  273. package/src/lib/shared/serializeEnv.ts +18 -0
  274. package/src/lib/shared/setCacheStoreResolver.ts +6 -0
  275. package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
  276. package/src/lib/shared/socketNameForFile.ts +11 -0
  277. package/src/lib/shared/sseErrorFrame.ts +29 -0
  278. package/src/lib/shared/streamResponse.ts +169 -0
  279. package/src/lib/shared/stripImport.ts +27 -0
  280. package/src/lib/shared/subscribableFromResponse.ts +51 -0
  281. package/src/lib/shared/toBunRoutePattern.ts +28 -0
  282. package/src/lib/shared/types/CacheEntry.ts +63 -0
  283. package/src/lib/shared/types/CacheInvalidation.ts +9 -0
  284. package/src/lib/shared/types/CacheOptions.ts +33 -0
  285. package/src/lib/shared/types/CacheSnapshot.ts +16 -0
  286. package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
  287. package/src/lib/shared/types/CacheStore.ts +32 -0
  288. package/src/lib/shared/types/ClientFlags.ts +11 -0
  289. package/src/lib/shared/types/CompileTarget.ts +6 -0
  290. package/src/lib/shared/types/HttpVerb.ts +1 -0
  291. package/src/lib/shared/types/LastConnection.ts +9 -0
  292. package/src/lib/shared/types/PromptArgument.ts +12 -0
  293. package/src/lib/shared/types/RawRemoteFunction.ts +13 -0
  294. package/src/lib/shared/types/RemoteFunction.ts +42 -0
  295. package/src/lib/shared/types/StandardSchemaV1.ts +57 -0
  296. package/src/lib/shared/types/StreamedResolution.ts +10 -0
  297. package/src/lib/shared/types/StreamingPlaceholder.ts +13 -0
  298. package/src/lib/shared/types/Subscribable.ts +15 -0
  299. package/src/lib/shared/types/SvelteConfig.ts +5 -0
  300. package/src/lib/shared/withJsonSchema.ts +20 -0
  301. package/src/lib/shared/writeLastConnection.ts +13 -0
  302. package/src/lib/shared/writeRoutesDts.ts +67 -0
  303. package/src/lib/test/clearVerbRegistry.ts +11 -0
  304. package/src/lib/test/createTestClient.ts +78 -0
  305. package/src/preload.ts +20 -0
  306. package/src/scaffold.ts +92 -0
  307. package/src/serverBuildPlugins.ts +25 -0
  308. package/src/serverEntry.ts +94 -0
  309. package/src/sveltePlugin.ts +58 -0
  310. package/src/tailwindStylePreprocessor.ts +62 -0
  311. package/template/bunfig.toml +4 -0
  312. package/template/package.json +19 -0
  313. package/template/src/app.ts +23 -0
  314. package/template/src/browser/app.css +21 -0
  315. package/template/src/browser/app.html +24 -0
  316. package/template/src/browser/pages/about/page.svelte +5 -0
  317. package/template/src/browser/pages/layout.svelte +26 -0
  318. package/template/src/browser/pages/page.svelte +20 -0
  319. package/template/src/bundle/icon.png +0 -0
  320. package/template/src/cli/banner.txt +3 -0
  321. package/template/src/cli/footer.txt +1 -0
  322. package/template/src/server/config.ts +17 -0
  323. package/template/src/server/rpc/getHello.ts +35 -0
  324. package/template/svelte.config.js +12 -0
  325. package/template/tsconfig.json +18 -0
  326. package/tsconfig.app.json +16 -0
@@ -0,0 +1,76 @@
1
+ /*
2
+ Minimal ustar tarball writer. Each entry is a 512-byte header followed
3
+ by the content padded to a 512-byte boundary; the archive ends with two
4
+ 512-byte zero blocks. After assembly the buffer is gzipped via
5
+ Bun.gzipSync — no external `tar` invocation, no extra deps.
6
+
7
+ Format constraints:
8
+ - File names ≤ 100 bytes (we never write longer paths).
9
+ - Numeric fields are zero-padded octal ASCII strings (POSIX rule).
10
+ - Checksum is the sum of all header bytes treating the checksum
11
+ field as spaces; encoded as 6 octal digits + NUL + space.
12
+ */
13
+
14
+ type TarEntry = {
15
+ name: string
16
+ content: Uint8Array
17
+ mode?: number
18
+ }
19
+
20
+ const BLOCK = 512
21
+ const ENC = new TextEncoder()
22
+
23
+ function writeString(buf: Uint8Array, offset: number, length: number, value: string): void {
24
+ const bytes = ENC.encode(value)
25
+ buf.set(bytes.subarray(0, Math.min(bytes.length, length)), offset)
26
+ }
27
+
28
+ function writeOctal(buf: Uint8Array, offset: number, length: number, value: number): void {
29
+ // length-1 octal digits + trailing NUL
30
+ const oct = value.toString(8).padStart(length - 1, '0')
31
+ writeString(buf, offset, length - 1, oct)
32
+ buf[offset + length - 1] = 0
33
+ }
34
+
35
+ function buildHeader(entry: TarEntry): Uint8Array {
36
+ const header = new Uint8Array(BLOCK)
37
+ writeString(header, 0, 100, entry.name)
38
+ writeOctal(header, 100, 8, entry.mode ?? 0o644)
39
+ writeOctal(header, 108, 8, 0)
40
+ writeOctal(header, 116, 8, 0)
41
+ writeOctal(header, 124, 12, entry.content.length)
42
+ writeOctal(header, 136, 12, Math.floor(Date.now() / 1000))
43
+ header.fill(0x20, 148, 156)
44
+ header[156] = 0x30 // '0' = regular file
45
+ writeString(header, 257, 6, 'ustar\0')
46
+ writeString(header, 263, 2, '00')
47
+ // Checksum: sum of all bytes with checksum field treated as spaces.
48
+ let sum = 0
49
+ for (let index = 0; index < BLOCK; index++) {
50
+ sum += header[index] ?? 0
51
+ }
52
+ writeOctal(header, 148, 7, sum)
53
+ header[155] = 0x20 // trailing space after checksum digits
54
+ return header
55
+ }
56
+
57
+ /*
58
+ Builds a gzipped tarball from the given entries and returns the bytes.
59
+ Sized eagerly (sum of headers + padded contents + 2 trailing blocks).
60
+ */
61
+ export function createTarGz(entries: TarEntry[]): Uint8Array {
62
+ let totalSize = BLOCK * 2 // trailing zero blocks
63
+ for (const entry of entries) {
64
+ totalSize += BLOCK
65
+ totalSize += Math.ceil(entry.content.length / BLOCK) * BLOCK
66
+ }
67
+ const tar = new Uint8Array(totalSize)
68
+ let offset = 0
69
+ for (const entry of entries) {
70
+ tar.set(buildHeader(entry), offset)
71
+ offset += BLOCK
72
+ tar.set(entry.content, offset)
73
+ offset += Math.ceil(entry.content.length / BLOCK) * BLOCK
74
+ }
75
+ return Bun.gzipSync(tar)
76
+ }
@@ -0,0 +1,153 @@
1
+ import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
2
+ import { exeSuffix } from '../../shared/exeSuffix.ts'
3
+ import { isCompileTarget } from '../../shared/isCompileTarget.ts'
4
+ import { log } from '../../shared/log.ts'
5
+ import { normalizeTarget } from '../../shared/normalizeTarget.ts'
6
+ import { buildEnvContent } from './buildEnvContent.ts'
7
+ import { createTarGz } from './createTarGz.ts'
8
+ import { maxSourceMtime } from './maxSourceMtime.ts'
9
+
10
+ // The sibling server binary's name for a platform — `server` / `server.exe` — must
11
+ // match what resolveServerBinary() looks for next to the unpacked CLI binary.
12
+ function serverBinaryName(platform: string): string {
13
+ return `server${exeSuffix(normalizeTarget(platform))}`
14
+ }
15
+
16
+ /*
17
+ Process-wide per-platform build coalescing. Two concurrent curls for
18
+ the same /__belte/cli/<platform> share one promise; the later requests
19
+ await the same one the first installed. The promise both runs the
20
+ freshness check AND the rebuild, so the map insertion is synchronous
21
+ relative to the first request's entry into the function — no window
22
+ between an `await` and `pendingBuilds.set` for a second concurrent
23
+ request to slip through and fire its own buildCli against the same
24
+ output paths.
25
+ */
26
+ const pendingBuilds = new Map<string, Promise<string | undefined>>()
27
+
28
+ async function ensurePlatformBinary(
29
+ platform: string,
30
+ programName: string,
31
+ cwd: string,
32
+ ): Promise<string | undefined> {
33
+ const existing = pendingBuilds.get(platform)
34
+ if (existing) {
35
+ return existing
36
+ }
37
+ const promise = computeBinary(platform, programName, cwd)
38
+ pendingBuilds.set(platform, promise)
39
+ /*
40
+ Drop the entry after settlement so a later request rebuilds if the
41
+ source has changed again. Identity-guard so a still-pending entry
42
+ installed by a follow-up request isn't evicted by ours.
43
+ */
44
+ promise.finally(() => {
45
+ if (pendingBuilds.get(platform) === promise) {
46
+ pendingBuilds.delete(platform)
47
+ }
48
+ })
49
+ return promise
50
+ }
51
+
52
+ async function computeBinary(
53
+ platform: string,
54
+ programName: string,
55
+ cwd: string,
56
+ ): Promise<string | undefined> {
57
+ const dir = `${cwd}/dist/cli-thin/${platform}`
58
+ const binaryPath = `${dir}/${programName}`
59
+ const serverPath = `${dir}/${serverBinaryName(platform)}`
60
+ /*
61
+ On-disk binaries are fresh when both the CLI and its sibling server exist AND
62
+ the CLI's mtime beats the newest rpc/socket source mtime. The mtime check
63
+ catches the common dev iteration where the user edits an rpc handler but didn't
64
+ run `belte cli` again; the server-exists check forces a rebuild for a dist
65
+ produced before the CLI co-shipped a server. Other source paths (project lib,
66
+ transitive imports) fall back to manual rebuild.
67
+ */
68
+ const binaryFile = Bun.file(binaryPath)
69
+ const serverFile = Bun.file(serverPath)
70
+ if ((await binaryFile.exists()) && (await serverFile.exists())) {
71
+ const binaryMtime = (await binaryFile.stat()).mtimeMs
72
+ const sourceMtime = await maxSourceMtime(cwd)
73
+ if (binaryMtime >= sourceMtime) {
74
+ return binaryPath
75
+ }
76
+ log.info(`thin cli for ${platform} is stale — rebuilding`)
77
+ }
78
+ try {
79
+ log.info(`lazy-building cli + server for ${platform}…`)
80
+ // Lazy-import buildCli so the build pipeline isn't pulled into
81
+ // production processes that never serve a download.
82
+ const { buildCli } = await import('../../../buildCli.ts')
83
+ await buildCli({
84
+ cwd,
85
+ platforms: [normalizeTarget(platform)],
86
+ })
87
+ return (await binaryFile.exists()) && (await serverFile.exists()) ? binaryPath : undefined
88
+ } catch (error) {
89
+ log.error(error)
90
+ return undefined
91
+ }
92
+ }
93
+
94
+ /*
95
+ Handles GET /__belte/cli/<platform> — streams a gzipped tarball
96
+ containing the platform-specific thin binary + a `.env` carrying
97
+ BELTE_APP_URL (and BELTE_APP_TOKEN if the inbound request was authenticated).
98
+
99
+ Thin binaries live at `dist/cli-thin/<platform>/<programName>`
100
+ (produced by `belte cli` with BELTE_APP_URL set). Missing platforms produce
101
+ 404 — the install script reports it, doesn't try to fall back.
102
+ */
103
+ export async function handleCliDownload(
104
+ request: Request,
105
+ platform: string,
106
+ programName: string,
107
+ cwd: string,
108
+ ): Promise<Response> {
109
+ /*
110
+ Validate the URL-supplied platform against the known target set before any
111
+ filesystem access or lazy build. Without this, an arbitrary `platform`
112
+ segment flows into `dist/cli-thin/<platform>` paths (a traversal/oracle
113
+ surface) and a cache miss triggers an expensive cross-compile — so spraying
114
+ distinct junk strings amplifies into unbounded concurrent builds.
115
+ */
116
+ if (!isCompileTarget(normalizeTarget(platform))) {
117
+ return new Response(`unknown platform: ${platform}`, {
118
+ status: 404,
119
+ headers: { 'Cache-Control': NO_STORE },
120
+ })
121
+ }
122
+ const binaryPath = await ensurePlatformBinary(platform, programName, cwd)
123
+ if (!binaryPath) {
124
+ return new Response(`unknown platform: ${platform}`, {
125
+ status: 404,
126
+ headers: { 'Cache-Control': NO_STORE },
127
+ })
128
+ }
129
+ const appUrl = new URL(request.url).origin
130
+ const auth = request.headers.get('authorization')
131
+ const bearer =
132
+ auth && auth.toLowerCase().startsWith('bearer ') ? auth.slice('bearer '.length) : undefined
133
+ const envContent = buildEnvContent(appUrl, bearer)
134
+
135
+ const serverPath = `${cwd}/dist/cli-thin/${platform}/${serverBinaryName(platform)}`
136
+ const [binaryBytes, serverBytes] = await Promise.all([
137
+ Bun.file(binaryPath).bytes(),
138
+ Bun.file(serverPath).bytes(),
139
+ ])
140
+ // Ship the server beside the CLI so `/start` can spawn a local instance.
141
+ const archive = createTarGz([
142
+ { name: programName, content: binaryBytes, mode: 0o755 },
143
+ { name: serverBinaryName(platform), content: serverBytes, mode: 0o755 },
144
+ { name: '.env', content: new TextEncoder().encode(envContent), mode: 0o644 },
145
+ ])
146
+ return new Response(archive, {
147
+ headers: {
148
+ 'Content-Type': 'application/gzip',
149
+ 'Content-Disposition': `attachment; filename="${programName}-${platform}.tar.gz"`,
150
+ 'Cache-Control': NO_STORE,
151
+ },
152
+ })
153
+ }
@@ -0,0 +1,37 @@
1
+ import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
2
+ import { installScript } from './installScript.ts'
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
+
15
+ /*
16
+ Handles GET /__belte/cli — returns the platform-detecting shell script.
17
+ Authoritative URL for the tarball is derived from the inbound request
18
+ (so the script's curl line points at whatever host the user reached us
19
+ on). Program name is the bundler-emitted `belte:cli-name` value.
20
+ */
21
+ export function handleCliInstall(request: Request, programName: string): Response {
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
+ }
29
+ const appUrl = url.origin
30
+ const script = installScript(appUrl, programName)
31
+ return new Response(script, {
32
+ headers: {
33
+ 'Content-Type': 'text/x-shellscript; charset=utf-8',
34
+ 'Cache-Control': NO_STORE,
35
+ },
36
+ })
37
+ }
@@ -0,0 +1,29 @@
1
+ /*
2
+ The shell script returned by `GET /__belte/cli` (no platform). Detects
3
+ uname OS + arch, normalises common arch aliases, then curls the right
4
+ platform-specific tarball and extracts it into `$BELTE_INSTALL_DIR`
5
+ (default `~/.local/bin`). The tarball already contains the `.env` next
6
+ to the binary — no separate config write step in the script.
7
+
8
+ The script is rendered server-side so `<BELTE_APP_URL>` is the request's own
9
+ origin and the embedded curl URL needs no escaping or quoting beyond
10
+ basic shell hygiene.
11
+ */
12
+ export function installScript(appUrl: string, programName: string): string {
13
+ return `#!/usr/bin/env sh
14
+ set -e
15
+ OS=$(uname -s | tr '[:upper:]' '[:lower:]')
16
+ case "$(uname -m)" in
17
+ x86_64|amd64) ARCH=x64 ;;
18
+ aarch64|arm64) ARCH=arm64 ;;
19
+ *) echo "unsupported architecture: $(uname -m)" >&2 ; exit 1 ;;
20
+ esac
21
+ INSTALL_DIR="\${BELTE_INSTALL_DIR:-$HOME/.local/bin}"
22
+ mkdir -p "$INSTALL_DIR"
23
+ URL="${appUrl.replace(/\/$/, '')}/__belte/cli/\${OS}-\${ARCH}"
24
+ echo "installing ${programName} from $URL into $INSTALL_DIR"
25
+ curl -fsSL "$URL" | tar -xz -C "$INSTALL_DIR"
26
+ echo "installed: $INSTALL_DIR/${programName}"
27
+ echo "ensure $INSTALL_DIR is in your PATH"
28
+ `
29
+ }
@@ -0,0 +1,26 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { Glob } from 'bun'
3
+
4
+ /*
5
+ Returns the most-recent mtime across every rpc + socket source file in
6
+ the project, or 0 when both directories are absent. The lazy CLI
7
+ download path compares this to the binary's mtime to decide whether to
8
+ rebuild — covers the common dev iteration of "user edited an rpc
9
+ handler" without needing to scan transitively-imported modules. Globs and
10
+ stats run concurrently since each file is independent.
11
+ */
12
+ export async function maxSourceMtime(cwd: string): Promise<number> {
13
+ const roots = [`${cwd}/src/server/rpc`, `${cwd}/src/server/sockets`].filter(existsSync)
14
+ const perRoot = await Promise.all(
15
+ roots.map(async (root) => {
16
+ const files = await Array.fromAsync(
17
+ new Glob('**/*.ts').scan({ cwd: root, onlyFiles: true }),
18
+ )
19
+ return files.map((file) => `${root}/${file}`)
20
+ }),
21
+ )
22
+ const mtimes = await Promise.all(
23
+ perRoot.flat().map(async (path) => (await Bun.file(path).stat()).mtimeMs),
24
+ )
25
+ return mtimes.reduce((newest, mtime) => Math.max(newest, mtime), 0)
26
+ }
@@ -0,0 +1,29 @@
1
+ import { requestContext } from './runtime/requestContext.ts'
2
+
3
+ /*
4
+ The cookie jar for the in-flight request. Reads parse the inbound `Cookie`
5
+ header; writes (`set` / `delete`) are collected and flushed to the response as
6
+ `Set-Cookie` headers when the handler returns (see runWithRequestScope). Backed
7
+ by Bun's native `CookieMap`, so it's a live `Map<string, string>` plus
8
+ `.set(name, value, options)` carrying the standard attributes (httpOnly, secure,
9
+ sameSite, maxAge, path, …) and `.delete(name)` for expiry:
10
+
11
+ const jar = cookies()
12
+ const session = jar.get('session') // read inbound
13
+ jar.set('session', token, { httpOnly: true, sameSite: 'lax' })
14
+ jar.delete('session') // expire on the way out
15
+
16
+ Materialized lazily on first call and cached on the request store, so a request
17
+ that never touches cookies parses nothing and emits no `Set-Cookie`. Throws
18
+ outside a request scope, like request().
19
+ */
20
+ export function cookies(): Bun.CookieMap {
21
+ const store = requestContext.getStore()
22
+ if (!store) {
23
+ throw new Error(
24
+ '[belte] cookies() called outside a request scope — it only resolves while an SSR render or rpc handler is in flight',
25
+ )
26
+ }
27
+ store.cookies ??= new Bun.CookieMap(store.req.headers.get('cookie') ?? '')
28
+ return store.cookies
29
+ }
@@ -0,0 +1,50 @@
1
+ import type { StandardSchemaV1 } from '../shared/types/StandardSchemaV1.ts'
2
+ import { envSchemaStore } from './runtime/envSchemaStore.ts'
3
+
4
+ /*
5
+ Validates the process environment against a Standard Schema and returns the
6
+ typed, parsed config. Built to be called at module top level so a missing or
7
+ malformed variable fails the boot loudly rather than surfacing as `undefined`
8
+ deep inside a handler:
9
+
10
+ // src/server/config.ts
11
+ export const config = env(
12
+ v.object({ DATABASE_URL: v.string(), STRIPE_KEY: v.string() }),
13
+ )
14
+
15
+ Reads `Bun.env` (the process environment plus any `.env` Bun loaded), so any
16
+ Standard Schema library — zod, valibot, arktype — works without an adapter,
17
+ same as the verb helpers. Coercion (e.g. a numeric PORT) lives in the schema.
18
+
19
+ The schema is registered (envSchemaStore) so the bundle launcher can project
20
+ the first-run setup form from the same declaration. When the launcher imports
21
+ this purely to read that schema it sets `skipValidation`, so env() registers
22
+ and returns without validating Bun.env — boot validation stays the server's.
23
+
24
+ Validation must be synchronous — boot can't await config — so a schema whose
25
+ `validate` returns a Promise throws. On failure every issue is reported at once
26
+ (path + message) so a misconfigured deploy shows the full list rather than one
27
+ variable per restart.
28
+ */
29
+ export function env<Schema extends StandardSchemaV1>(
30
+ schema: Schema,
31
+ ): StandardSchemaV1.InferOutput<Schema> {
32
+ envSchemaStore.schema = schema
33
+ if (envSchemaStore.skipValidation) {
34
+ return Bun.env as unknown as StandardSchemaV1.InferOutput<Schema>
35
+ }
36
+ const result = schema['~standard'].validate(Bun.env)
37
+ if (result instanceof Promise) {
38
+ throw new Error('[belte] env() schema must validate synchronously')
39
+ }
40
+ if (result.issues) {
41
+ const lines = result.issues.map((issue) => {
42
+ const path = issue.path
43
+ ?.map((segment) => String(typeof segment === 'object' ? segment.key : segment))
44
+ .join('.')
45
+ return path ? ` ${path}: ${issue.message}` : ` ${issue.message}`
46
+ })
47
+ throw new Error(`[belte] invalid environment:\n${lines.join('\n')}`)
48
+ }
49
+ return result.value
50
+ }
@@ -0,0 +1,70 @@
1
+ import { NO_STORE } from '../shared/CACHE_CONTROL_VALUES.ts'
2
+ import type { TypedResponse } from './rpc/types/TypedResponse.ts'
3
+ import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
4
+
5
+ /*
6
+ Plain-text error Response — clearer than constructing a Response by
7
+ hand with a status and a text body, and shaped so the client's
8
+ HttpError carries the message verbatim (`HttpError.response.text()`
9
+ returns the message, no parsing).
10
+
11
+ if (!order) return error(404, 'order not found')
12
+
13
+ `message` defaults to the status's standard reason phrase when
14
+ omitted (e.g. `error(404)` body = 'Not Found'). The body is
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`.
18
+
19
+ To short-circuit a handler instead of returning, `throw new Error(...)`
20
+ or `throw new HttpError(error(...))` — the framework's `app.handleError`
21
+ hook catches thrown errors. This helper deliberately returns a Response
22
+ rather than throwing one so a single `return error(...)` is the
23
+ expected pattern, with the same control flow as `return json(...)`.
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
+ */
32
+ const STATUS_TEXT: Record<number, string> = {
33
+ 400: 'Bad Request',
34
+ 401: 'Unauthorized',
35
+ 403: 'Forbidden',
36
+ 404: 'Not Found',
37
+ 405: 'Method Not Allowed',
38
+ 409: 'Conflict',
39
+ 410: 'Gone',
40
+ 422: 'Unprocessable Content',
41
+ 429: 'Too Many Requests',
42
+ 500: 'Internal Server Error',
43
+ 501: 'Not Implemented',
44
+ 502: 'Bad Gateway',
45
+ 503: 'Service Unavailable',
46
+ 504: 'Gateway Timeout',
47
+ }
48
+
49
+ /*
50
+ Body type is `never` because `error()` only travels the non-2xx path on
51
+ the wire — the caller's `await fn(args)` throws `HttpError` and never
52
+ resolves to this response's body. Returning a TypedResponse<never> lets
53
+ the union of branches in a handler narrow to whatever the success
54
+ branch carries (`TypedResponse<{user}> | TypedResponse<never>` → Return
55
+ = {user}).
56
+ */
57
+ export function error(status: number, message?: string, init?: ResponseInit): TypedResponse<never> {
58
+ const body = message ?? STATUS_TEXT[status] ?? `HTTP ${status}`
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>
70
+ }
@@ -0,0 +1,28 @@
1
+ import { NO_STORE } from '../shared/CACHE_CONTROL_VALUES.ts'
2
+ import type { TypedResponse } from './rpc/types/TypedResponse.ts'
3
+ import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
4
+
5
+ /*
6
+ JSON Response with rpc-friendly defaults — same shape as
7
+ `Response.json(data, init)`, except `Cache-Control: no-store` is set
8
+ unless the caller overrides it. Intermediary caches (browsers, CDNs,
9
+ shared proxies) shouldn't cache rpc replies by default; the framework's
10
+ own per-request cache handles in-process dedupe.
11
+
12
+ export const getOrder = GET<{ id: string }>(async ({ id }) =>
13
+ json(await db.getOrder(id)),
14
+ )
15
+
16
+ The return type carries `T` as a phantom brand so the verb helper can
17
+ infer the caller-facing `Return` from the handler body — no need to
18
+ annotate `GET<Args, Return>` just to type the response shape.
19
+
20
+ For non-default cache policy pass `init.headers`; explicit
21
+ `cache-control` wins over the default.
22
+ */
23
+ export function json<T>(data: T, init?: ResponseInit): TypedResponse<T> {
24
+ return Response.json(
25
+ data,
26
+ withResponseDefaults(init, { 'Cache-Control': NO_STORE }),
27
+ ) as TypedResponse<T>
28
+ }
@@ -0,0 +1,46 @@
1
+ /*
2
+ Wraps an AsyncIterable<Frame> in a Response whose body is JSON Lines
3
+ (application/jsonl) — one JSON value per line, terminated by `\n`. Used
4
+ inside an rpc handler to turn a generator into a streaming HTTP response
5
+ that `subscribe(fn.stream)(args)` consumes frame-by-frame on the client.
6
+
7
+ export const orderFeed = GET<Args>((args) =>
8
+ jsonl(async function* () {
9
+ for await (const order of db.watchOrders(args)) yield order
10
+ }())
11
+ )
12
+
13
+ Cancellation flows from the consumer through ReadableStream's `cancel`
14
+ into `iter.return()` so the handler's `for await` exits via its normal
15
+ control path (DB cursors, file handles, etc. get to release in finally).
16
+
17
+ Errors thrown by the generator are emitted as a final
18
+ `{"$error":"<message>"}` line before the stream closes. The convention
19
+ keeps the format JSON-safe and lets the consumer distinguish "stream
20
+ ended cleanly" from "handler threw" without a side-channel. The full
21
+ error is logged server-side via the framework's error handler — only the
22
+ message crosses the wire.
23
+ */
24
+ import { NO_STORE } from '../shared/CACHE_CONTROL_VALUES.ts'
25
+ import { jsonlErrorFrame } from '../shared/jsonlErrorFrame.ts'
26
+ import type { TypedResponse } from './rpc/types/TypedResponse.ts'
27
+ import { streamFromIterator } from './runtime/streamFromIterator.ts'
28
+ import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
29
+
30
+ export function jsonl<Frame>(
31
+ iterable: AsyncIterable<Frame>,
32
+ init?: ResponseInit,
33
+ ): TypedResponse<Frame> {
34
+ const body = streamFromIterator(iterable, {
35
+ encodeFrame: (value) => `${JSON.stringify(value)}\n`,
36
+ encodeError: (message) => jsonlErrorFrame.encode(message),
37
+ })
38
+ return new Response(
39
+ body,
40
+ withResponseDefaults(init, {
41
+ 'Content-Type': 'application/jsonl; charset=utf-8',
42
+ 'Cache-Control': NO_STORE,
43
+ 'X-Content-Type-Options': 'nosniff',
44
+ }),
45
+ ) as TypedResponse<Frame>
46
+ }
@@ -0,0 +1,20 @@
1
+ import { registerPrompt } from './registerPrompt.ts'
2
+ import type { Prompt } from './types/Prompt.ts'
3
+ import type { PromptOptions } from './types/PromptOptions.ts'
4
+
5
+ /*
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
+ */
12
+ export function definePrompt(name: string, opts: PromptOptions): Prompt {
13
+ const self: Prompt = {
14
+ name,
15
+ description: opts.description,
16
+ render: opts.render,
17
+ }
18
+ registerPrompt({ prompt: self, jsonSchema: opts.jsonSchema })
19
+ return self
20
+ }
@@ -0,0 +1,9 @@
1
+ import type { PromptRegistryEntry } from './types/PromptRegistryEntry.ts'
2
+
3
+ /*
4
+ Process-wide registry of every prompt declared in the app. definePrompt
5
+ inserts on first construction (eagerly when the registry loader walks the
6
+ prompts manifest at MCP boot). The MCP server reads this to build its
7
+ `prompts/list` + `prompts/get` responses.
8
+ */
9
+ export const promptRegistry = new Map<string, PromptRegistryEntry>()
@@ -0,0 +1,6 @@
1
+ import { promptRegistry } from './promptRegistry.ts'
2
+ import type { PromptRegistryEntry } from './types/PromptRegistryEntry.ts'
3
+
4
+ export function registerPrompt(entry: PromptRegistryEntry): void {
5
+ promptRegistry.set(entry.prompt.name, entry)
6
+ }
@@ -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 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
+ }
@@ -0,0 +1,13 @@
1
+ /*
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.
8
+ */
9
+ export type Prompt = {
10
+ readonly name: string
11
+ readonly description: string | undefined
12
+ render(args: Record<string, string>): string
13
+ }
@@ -0,0 +1,12 @@
1
+ /*
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.
7
+ */
8
+ export type PromptOptions = {
9
+ description?: string
10
+ jsonSchema?: Record<string, unknown>
11
+ render: (args: Record<string, string>) => string
12
+ }
@@ -0,0 +1,13 @@
1
+ import type { Prompt } from './Prompt.ts'
2
+
3
+ /*
4
+ Per-prompt registry record. The MCP dispatcher enumerates this to build
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
8
+ burdened with metadata it never reads.
9
+ */
10
+ export type PromptRegistryEntry = {
11
+ prompt: Prompt
12
+ jsonSchema: Record<string, unknown> | undefined
13
+ }
@@ -0,0 +1,10 @@
1
+ import type { Prompt } from './Prompt.ts'
2
+
3
+ /*
4
+ Manifest of prompt-name → module loader. Produced by the resolver plugin
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
+ */
10
+ export type PromptRoutes = Record<string, () => Promise<Record<string, Prompt>>>