@soederpop/luca 0.0.2

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 (358) hide show
  1. package/CLAUDE.md +71 -0
  2. package/README.md +78 -0
  3. package/bun.lock +2928 -0
  4. package/bunfig.toml +3 -0
  5. package/commands/audit-docs.ts +740 -0
  6. package/commands/build-scaffolds.ts +154 -0
  7. package/commands/generate-api-docs.ts +114 -0
  8. package/commands/update-introspection.ts +67 -0
  9. package/docs/CLI.md +335 -0
  10. package/docs/README.md +88 -0
  11. package/docs/TABLE-OF-CONTENTS.md +157 -0
  12. package/docs/apis/clients/elevenlabs.md +84 -0
  13. package/docs/apis/clients/graph.md +56 -0
  14. package/docs/apis/clients/openai.md +69 -0
  15. package/docs/apis/clients/rest.md +41 -0
  16. package/docs/apis/clients/websocket.md +107 -0
  17. package/docs/apis/features/agi/assistant.md +471 -0
  18. package/docs/apis/features/agi/assistants-manager.md +154 -0
  19. package/docs/apis/features/agi/claude-code.md +602 -0
  20. package/docs/apis/features/agi/conversation-history.md +352 -0
  21. package/docs/apis/features/agi/conversation.md +333 -0
  22. package/docs/apis/features/agi/docs-reader.md +121 -0
  23. package/docs/apis/features/agi/openai-codex.md +318 -0
  24. package/docs/apis/features/agi/openapi.md +138 -0
  25. package/docs/apis/features/agi/semantic-search.md +387 -0
  26. package/docs/apis/features/agi/skills-library.md +216 -0
  27. package/docs/apis/features/node/container-link.md +133 -0
  28. package/docs/apis/features/node/content-db.md +313 -0
  29. package/docs/apis/features/node/disk-cache.md +379 -0
  30. package/docs/apis/features/node/dns.md +651 -0
  31. package/docs/apis/features/node/docker.md +705 -0
  32. package/docs/apis/features/node/downloader.md +81 -0
  33. package/docs/apis/features/node/esbuild.md +59 -0
  34. package/docs/apis/features/node/file-manager.md +182 -0
  35. package/docs/apis/features/node/fs.md +581 -0
  36. package/docs/apis/features/node/git.md +330 -0
  37. package/docs/apis/features/node/google-auth.md +174 -0
  38. package/docs/apis/features/node/google-calendar.md +187 -0
  39. package/docs/apis/features/node/google-docs.md +151 -0
  40. package/docs/apis/features/node/google-drive.md +225 -0
  41. package/docs/apis/features/node/google-sheets.md +179 -0
  42. package/docs/apis/features/node/grep.md +290 -0
  43. package/docs/apis/features/node/helpers.md +135 -0
  44. package/docs/apis/features/node/ink.md +334 -0
  45. package/docs/apis/features/node/ipc-socket.md +260 -0
  46. package/docs/apis/features/node/json-tree.md +86 -0
  47. package/docs/apis/features/node/launcher-app-command-listener.md +145 -0
  48. package/docs/apis/features/node/networking.md +281 -0
  49. package/docs/apis/features/node/nlp.md +133 -0
  50. package/docs/apis/features/node/opener.md +97 -0
  51. package/docs/apis/features/node/os.md +118 -0
  52. package/docs/apis/features/node/package-finder.md +402 -0
  53. package/docs/apis/features/node/postgres.md +212 -0
  54. package/docs/apis/features/node/proc.md +430 -0
  55. package/docs/apis/features/node/process-manager.md +210 -0
  56. package/docs/apis/features/node/python.md +278 -0
  57. package/docs/apis/features/node/repl.md +88 -0
  58. package/docs/apis/features/node/runpod.md +673 -0
  59. package/docs/apis/features/node/secure-shell.md +169 -0
  60. package/docs/apis/features/node/semantic-search.md +401 -0
  61. package/docs/apis/features/node/sqlite.md +211 -0
  62. package/docs/apis/features/node/telegram.md +254 -0
  63. package/docs/apis/features/node/tts.md +118 -0
  64. package/docs/apis/features/node/ui.md +703 -0
  65. package/docs/apis/features/node/vault.md +64 -0
  66. package/docs/apis/features/node/vm.md +84 -0
  67. package/docs/apis/features/node/window-manager.md +337 -0
  68. package/docs/apis/features/node/yaml-tree.md +85 -0
  69. package/docs/apis/features/node/yaml.md +176 -0
  70. package/docs/apis/features/web/asset-loader.md +47 -0
  71. package/docs/apis/features/web/container-link.md +133 -0
  72. package/docs/apis/features/web/esbuild.md +59 -0
  73. package/docs/apis/features/web/helpers.md +135 -0
  74. package/docs/apis/features/web/network.md +30 -0
  75. package/docs/apis/features/web/speech.md +55 -0
  76. package/docs/apis/features/web/vault.md +64 -0
  77. package/docs/apis/features/web/vm.md +84 -0
  78. package/docs/apis/features/web/voice.md +67 -0
  79. package/docs/apis/servers/express.md +127 -0
  80. package/docs/apis/servers/mcp.md +213 -0
  81. package/docs/apis/servers/websocket.md +99 -0
  82. package/docs/documentation-audit.md +134 -0
  83. package/docs/examples/content-db.md +77 -0
  84. package/docs/examples/disk-cache.md +83 -0
  85. package/docs/examples/docker.md +101 -0
  86. package/docs/examples/downloader.md +70 -0
  87. package/docs/examples/esbuild.md +80 -0
  88. package/docs/examples/file-manager.md +82 -0
  89. package/docs/examples/fs.md +83 -0
  90. package/docs/examples/git.md +85 -0
  91. package/docs/examples/google-auth.md +88 -0
  92. package/docs/examples/google-calendar.md +94 -0
  93. package/docs/examples/google-docs.md +82 -0
  94. package/docs/examples/google-drive.md +96 -0
  95. package/docs/examples/google-sheets.md +95 -0
  96. package/docs/examples/grep.md +85 -0
  97. package/docs/examples/ink-blocks.md +75 -0
  98. package/docs/examples/ink-renderer.md +41 -0
  99. package/docs/examples/ink.md +103 -0
  100. package/docs/examples/ipc-socket.md +103 -0
  101. package/docs/examples/json-tree.md +91 -0
  102. package/docs/examples/launcher-app-command-listener.md +120 -0
  103. package/docs/examples/networking.md +58 -0
  104. package/docs/examples/nlp.md +91 -0
  105. package/docs/examples/opener.md +78 -0
  106. package/docs/examples/os.md +72 -0
  107. package/docs/examples/package-finder.md +89 -0
  108. package/docs/examples/port-exposer.md +89 -0
  109. package/docs/examples/postgres.md +91 -0
  110. package/docs/examples/proc.md +81 -0
  111. package/docs/examples/process-manager.md +79 -0
  112. package/docs/examples/python.md +91 -0
  113. package/docs/examples/repl.md +93 -0
  114. package/docs/examples/runpod.md +119 -0
  115. package/docs/examples/secure-shell.md +92 -0
  116. package/docs/examples/sqlite.md +86 -0
  117. package/docs/examples/telegram.md +77 -0
  118. package/docs/examples/tts.md +86 -0
  119. package/docs/examples/ui.md +80 -0
  120. package/docs/examples/vault.md +70 -0
  121. package/docs/examples/vm.md +86 -0
  122. package/docs/examples/window-manager.md +125 -0
  123. package/docs/examples/yaml-tree.md +93 -0
  124. package/docs/examples/yaml.md +104 -0
  125. package/docs/ideas/class-registration-refactor-possibilities.md +197 -0
  126. package/docs/ideas/container-use-api.md +9 -0
  127. package/docs/ideas/easy-auth-for-express-servers-and-luca-serve.md +0 -0
  128. package/docs/ideas/feature-stacks.md +22 -0
  129. package/docs/ideas/luca-cli-self-sufficiency-demo.md +23 -0
  130. package/docs/ideas/mcp-design.md +9 -0
  131. package/docs/ideas/web-container-debugging-feature.md +13 -0
  132. package/docs/introspection-audit.md +49 -0
  133. package/docs/introspection.md +154 -0
  134. package/docs/mcp/readme.md +162 -0
  135. package/docs/models.ts +38 -0
  136. package/docs/philosophy.md +85 -0
  137. package/docs/principles.md +7 -0
  138. package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +34 -0
  139. package/docs/prompts/mcp-test-easy-command.md +27 -0
  140. package/docs/reports/assistant-bugs.md +38 -0
  141. package/docs/reports/attach-pattern-usage.md +18 -0
  142. package/docs/reports/code-audit-results.md +391 -0
  143. package/docs/reports/introspection-audit-tasks.md +378 -0
  144. package/docs/reports/luca-mcp-improvements.md +128 -0
  145. package/docs/scaffolds/client.md +140 -0
  146. package/docs/scaffolds/command.md +106 -0
  147. package/docs/scaffolds/endpoint.md +176 -0
  148. package/docs/scaffolds/feature.md +148 -0
  149. package/docs/scaffolds/server.md +187 -0
  150. package/docs/tasks/web-container-helper-discovery.md +71 -0
  151. package/docs/todos.md +1 -0
  152. package/docs/tutorials/01-getting-started.md +106 -0
  153. package/docs/tutorials/02-container.md +210 -0
  154. package/docs/tutorials/03-scripts.md +194 -0
  155. package/docs/tutorials/04-features-overview.md +196 -0
  156. package/docs/tutorials/05-state-and-events.md +171 -0
  157. package/docs/tutorials/06-servers.md +157 -0
  158. package/docs/tutorials/07-endpoints.md +198 -0
  159. package/docs/tutorials/08-commands.md +171 -0
  160. package/docs/tutorials/09-clients.md +162 -0
  161. package/docs/tutorials/10-creating-features.md +198 -0
  162. package/docs/tutorials/11-contentbase.md +191 -0
  163. package/docs/tutorials/12-assistants.md +215 -0
  164. package/docs/tutorials/13-introspection.md +147 -0
  165. package/docs/tutorials/14-type-system.md +174 -0
  166. package/docs/tutorials/15-project-patterns.md +222 -0
  167. package/docs/tutorials/16-google-features.md +534 -0
  168. package/docs/tutorials/17-tui-blocks.md +530 -0
  169. package/docs/tutorials/18-semantic-search.md +334 -0
  170. package/index.ts +1 -0
  171. package/luca.console.ts +9 -0
  172. package/main.py +6 -0
  173. package/package.json +154 -0
  174. package/pyproject.toml +7 -0
  175. package/scripts/animations/chrome-glitch.ts +55 -0
  176. package/scripts/animations/index.ts +16 -0
  177. package/scripts/animations/neon-pulse.ts +64 -0
  178. package/scripts/animations/types.ts +6 -0
  179. package/scripts/build-web.ts +28 -0
  180. package/scripts/examples/ask-luca-expert.ts +42 -0
  181. package/scripts/examples/assistant-questions.ts +12 -0
  182. package/scripts/examples/excalidraw-expert.ts +75 -0
  183. package/scripts/examples/expert-chat.ts +0 -0
  184. package/scripts/examples/file-manager.ts +14 -0
  185. package/scripts/examples/ideas.ts +12 -0
  186. package/scripts/examples/interactive-chat.ts +20 -0
  187. package/scripts/examples/openai-tool-calls.ts +113 -0
  188. package/scripts/examples/opening-a-web-browser.ts +5 -0
  189. package/scripts/examples/telegram-bot.ts +79 -0
  190. package/scripts/examples/telegram-ink-ui.ts +302 -0
  191. package/scripts/examples/using-assistant-with-mcp.ts +560 -0
  192. package/scripts/examples/using-claude-code.ts +10 -0
  193. package/scripts/examples/using-contentdb.ts +35 -0
  194. package/scripts/examples/using-conversations.ts +35 -0
  195. package/scripts/examples/using-disk-cache.ts +10 -0
  196. package/scripts/examples/using-docker-shell.ts +75 -0
  197. package/scripts/examples/using-elevenlabs.ts +25 -0
  198. package/scripts/examples/using-google-calendar.ts +57 -0
  199. package/scripts/examples/using-google-docs.ts +74 -0
  200. package/scripts/examples/using-google-drive.ts +74 -0
  201. package/scripts/examples/using-google-sheets.ts +89 -0
  202. package/scripts/examples/using-nlp.ts +55 -0
  203. package/scripts/examples/using-ollama.ts +10 -0
  204. package/scripts/examples/using-openai-codex.ts +23 -0
  205. package/scripts/examples/using-postgres.ts +55 -0
  206. package/scripts/examples/using-runpod.ts +32 -0
  207. package/scripts/examples/using-tts.ts +40 -0
  208. package/scripts/examples/vm-loading-esm-modules.ts +16 -0
  209. package/scripts/scaffold.ts +391 -0
  210. package/scripts/scratch.ts +15 -0
  211. package/scripts/test-command-listener.ts +123 -0
  212. package/scripts/test-window-manager-lifecycle.ts +86 -0
  213. package/scripts/test-window-manager.ts +43 -0
  214. package/scripts/update-introspection-data.ts +58 -0
  215. package/src/agi/README.md +14 -0
  216. package/src/agi/container.server.ts +114 -0
  217. package/src/agi/endpoints/ask.ts +60 -0
  218. package/src/agi/endpoints/conversations/[id].ts +45 -0
  219. package/src/agi/endpoints/conversations.ts +31 -0
  220. package/src/agi/endpoints/experts.ts +37 -0
  221. package/src/agi/features/assistant.ts +767 -0
  222. package/src/agi/features/assistants-manager.ts +260 -0
  223. package/src/agi/features/claude-code.ts +1111 -0
  224. package/src/agi/features/conversation-history.ts +497 -0
  225. package/src/agi/features/conversation.ts +799 -0
  226. package/src/agi/features/openai-codex.ts +631 -0
  227. package/src/agi/features/openapi.ts +438 -0
  228. package/src/agi/features/skills-library.ts +425 -0
  229. package/src/agi/index.ts +6 -0
  230. package/src/agi/lib/token-counter.ts +122 -0
  231. package/src/browser.ts +25 -0
  232. package/src/bus.ts +100 -0
  233. package/src/cli/cli.ts +70 -0
  234. package/src/client.ts +461 -0
  235. package/src/clients/civitai/index.ts +541 -0
  236. package/src/clients/client-template.ts +41 -0
  237. package/src/clients/comfyui/index.ts +597 -0
  238. package/src/clients/elevenlabs/index.ts +291 -0
  239. package/src/clients/openai/index.ts +451 -0
  240. package/src/clients/supabase/index.ts +366 -0
  241. package/src/command.ts +164 -0
  242. package/src/commands/chat.ts +182 -0
  243. package/src/commands/console.ts +192 -0
  244. package/src/commands/describe.ts +433 -0
  245. package/src/commands/eval.ts +116 -0
  246. package/src/commands/help.ts +214 -0
  247. package/src/commands/index.ts +14 -0
  248. package/src/commands/mcp.ts +64 -0
  249. package/src/commands/prompt.ts +807 -0
  250. package/src/commands/run.ts +257 -0
  251. package/src/commands/sandbox-mcp.ts +439 -0
  252. package/src/commands/scaffold.ts +79 -0
  253. package/src/commands/serve.ts +172 -0
  254. package/src/container.ts +781 -0
  255. package/src/endpoint.ts +340 -0
  256. package/src/feature.ts +75 -0
  257. package/src/hash-object.ts +97 -0
  258. package/src/helper.ts +543 -0
  259. package/src/introspection/generated.agi.ts +23388 -0
  260. package/src/introspection/generated.node.ts +18899 -0
  261. package/src/introspection/generated.web.ts +2021 -0
  262. package/src/introspection/index.ts +256 -0
  263. package/src/introspection/scan.ts +912 -0
  264. package/src/node/container.ts +354 -0
  265. package/src/node/feature.ts +13 -0
  266. package/src/node/features/container-link.ts +558 -0
  267. package/src/node/features/content-db.ts +475 -0
  268. package/src/node/features/disk-cache.ts +382 -0
  269. package/src/node/features/dns.ts +655 -0
  270. package/src/node/features/docker.ts +912 -0
  271. package/src/node/features/downloader.ts +92 -0
  272. package/src/node/features/esbuild.ts +68 -0
  273. package/src/node/features/file-manager.ts +357 -0
  274. package/src/node/features/fs.ts +534 -0
  275. package/src/node/features/git.ts +492 -0
  276. package/src/node/features/google-auth.ts +502 -0
  277. package/src/node/features/google-calendar.ts +300 -0
  278. package/src/node/features/google-docs.ts +404 -0
  279. package/src/node/features/google-drive.ts +339 -0
  280. package/src/node/features/google-sheets.ts +279 -0
  281. package/src/node/features/grep.ts +406 -0
  282. package/src/node/features/helpers.ts +374 -0
  283. package/src/node/features/ink.ts +490 -0
  284. package/src/node/features/ipc-socket.ts +459 -0
  285. package/src/node/features/json-tree.ts +188 -0
  286. package/src/node/features/launcher-app-command-listener.ts +388 -0
  287. package/src/node/features/networking.ts +925 -0
  288. package/src/node/features/nlp.ts +211 -0
  289. package/src/node/features/opener.ts +166 -0
  290. package/src/node/features/os.ts +157 -0
  291. package/src/node/features/package-finder.ts +539 -0
  292. package/src/node/features/port-exposer.ts +342 -0
  293. package/src/node/features/postgres.ts +273 -0
  294. package/src/node/features/proc.ts +502 -0
  295. package/src/node/features/process-manager.ts +542 -0
  296. package/src/node/features/python.ts +444 -0
  297. package/src/node/features/repl.ts +194 -0
  298. package/src/node/features/runpod.ts +802 -0
  299. package/src/node/features/secure-shell.ts +248 -0
  300. package/src/node/features/semantic-search.ts +924 -0
  301. package/src/node/features/sqlite.ts +289 -0
  302. package/src/node/features/telegram.ts +342 -0
  303. package/src/node/features/tts.ts +184 -0
  304. package/src/node/features/ui.ts +857 -0
  305. package/src/node/features/vault.ts +164 -0
  306. package/src/node/features/vm.ts +312 -0
  307. package/src/node/features/window-manager.ts +804 -0
  308. package/src/node/features/yaml-tree.ts +149 -0
  309. package/src/node/features/yaml.ts +132 -0
  310. package/src/node.ts +70 -0
  311. package/src/react/index.ts +175 -0
  312. package/src/registry.ts +199 -0
  313. package/src/scaffolds/generated.ts +1613 -0
  314. package/src/scaffolds/template.ts +37 -0
  315. package/src/schemas/base.ts +255 -0
  316. package/src/server.ts +135 -0
  317. package/src/servers/express.ts +209 -0
  318. package/src/servers/mcp.ts +805 -0
  319. package/src/servers/socket.ts +120 -0
  320. package/src/state.ts +101 -0
  321. package/src/web/clients/socket.ts +82 -0
  322. package/src/web/container.ts +74 -0
  323. package/src/web/extension.ts +30 -0
  324. package/src/web/feature.ts +12 -0
  325. package/src/web/features/asset-loader.ts +64 -0
  326. package/src/web/features/container-link.ts +385 -0
  327. package/src/web/features/esbuild.ts +79 -0
  328. package/src/web/features/helpers.ts +267 -0
  329. package/src/web/features/network.ts +61 -0
  330. package/src/web/features/speech.ts +87 -0
  331. package/src/web/features/vault.ts +189 -0
  332. package/src/web/features/vm.ts +78 -0
  333. package/src/web/features/voice-recognition.ts +129 -0
  334. package/src/web/shims/isomorphic-vm.ts +149 -0
  335. package/test/bus.test.ts +134 -0
  336. package/test/clients-servers.test.ts +216 -0
  337. package/test/container-link.test.ts +274 -0
  338. package/test/features.test.ts +160 -0
  339. package/test/integration.test.ts +787 -0
  340. package/test/node-container.test.ts +121 -0
  341. package/test/rate-limit.test.ts +272 -0
  342. package/test/semantic-search.test.ts +550 -0
  343. package/test/state.test.ts +121 -0
  344. package/test-integration/assistant.test.ts +138 -0
  345. package/test-integration/assistants-manager.test.ts +123 -0
  346. package/test-integration/claude-code.test.ts +98 -0
  347. package/test-integration/conversation-history.test.ts +205 -0
  348. package/test-integration/conversation.test.ts +137 -0
  349. package/test-integration/elevenlabs.test.ts +55 -0
  350. package/test-integration/google-services.test.ts +80 -0
  351. package/test-integration/helpers.ts +89 -0
  352. package/test-integration/openai-codex.test.ts +93 -0
  353. package/test-integration/runpod.test.ts +58 -0
  354. package/test-integration/server-endpoints.test.ts +97 -0
  355. package/test-integration/skills-library.test.ts +157 -0
  356. package/test-integration/telegram.test.ts +46 -0
  357. package/tsconfig.json +58 -0
  358. package/uv.lock +8 -0
@@ -0,0 +1,342 @@
1
+ import { z } from 'zod'
2
+ import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
3
+ import * as ngrok from '@ngrok/ngrok'
4
+ import { Feature, features } from '../../feature.js'
5
+
6
+ /**
7
+ * Zod schema for the Port Exposer feature state
8
+ */
9
+ export const PortExposerStateSchema = FeatureStateSchema.extend({
10
+ /** Whether ngrok is currently connected */
11
+ connected: z.boolean().describe('Whether ngrok is currently connected'),
12
+ /** The public URL provided by ngrok */
13
+ publicUrl: z.string().optional().describe('The public URL provided by ngrok'),
14
+ /** The local port being exposed */
15
+ localPort: z.number().optional().describe('The local port being exposed'),
16
+ /** Ngrok session information */
17
+ sessionInfo: z.object({
18
+ authToken: z.string().optional().describe('Ngrok authentication token'),
19
+ region: z.string().optional().describe('Ngrok region'),
20
+ subdomain: z.string().optional().describe('Ngrok subdomain'),
21
+ }).optional().describe('Ngrok session information'),
22
+ /** Connection timestamp */
23
+ connectedAt: z.coerce.date().optional().describe('Timestamp when the connection was established'),
24
+ /** Any error that occurred */
25
+ lastError: z.string().optional().describe('Last error message from an ngrok operation'),
26
+ })
27
+ export type PortExposerState = z.infer<typeof PortExposerStateSchema>
28
+
29
+ /**
30
+ * Zod schema for Port Exposer feature options
31
+ */
32
+ export const PortExposerOptionsSchema = FeatureOptionsSchema.extend({
33
+ /** Local port to expose */
34
+ port: z.number().optional().describe('Local port to expose'),
35
+ /** Optional ngrok auth token for premium features */
36
+ authToken: z.string().optional().describe('Ngrok auth token for premium features'),
37
+ /** Preferred region (us, eu, ap, au, sa, jp, in) */
38
+ region: z.string().optional().describe('Preferred ngrok region (us, eu, ap, au, sa, jp, in)'),
39
+ /** Custom subdomain (requires paid plan) */
40
+ subdomain: z.string().optional().describe('Custom subdomain (requires paid plan)'),
41
+ /** Domain to use (requires paid plan) */
42
+ domain: z.string().optional().describe('Domain to use (requires paid plan)'),
43
+ /** Basic auth for the tunnel */
44
+ basicAuth: z.string().optional().describe('Basic auth credentials for the tunnel'),
45
+ /** OAuth provider for authentication */
46
+ oauth: z.string().optional().describe('OAuth provider for authentication'),
47
+ /** Additional ngrok configuration */
48
+ config: z.any().describe('Additional ngrok configuration'),
49
+ })
50
+ export type PortExposerOptions = z.infer<typeof PortExposerOptionsSchema>
51
+
52
+ /**
53
+ * Port Exposer Feature
54
+ *
55
+ * Exposes local HTTP services via ngrok with SSL-enabled public URLs.
56
+ * Perfect for development, testing, and sharing local services securely.
57
+ *
58
+ * Features:
59
+ * - SSL-enabled public URLs for local services
60
+ * - Custom subdomains and domains (with paid plans)
61
+ * - Authentication options (basic auth, OAuth)
62
+ * - Regional endpoint selection
63
+ * - Connection state management
64
+ *
65
+ * @example
66
+ * ```typescript
67
+ * // Basic usage
68
+ * const exposer = container.feature('portExposer', { port: 3000 })
69
+ * const url = await exposer.expose()
70
+ * console.log(`Service available at: ${url}`)
71
+ *
72
+ * // With custom subdomain
73
+ * const exposer = container.feature('portExposer', {
74
+ * port: 8080,
75
+ * subdomain: 'my-app',
76
+ * authToken: 'your-ngrok-token'
77
+ * })
78
+ * ```
79
+ */
80
+ export class PortExposer extends Feature<PortExposerState, PortExposerOptions> {
81
+ static override shortcut = 'portExposer' as const
82
+ static override stateSchema = PortExposerStateSchema
83
+ static override optionsSchema = PortExposerOptionsSchema
84
+
85
+ private ngrokListener?: ngrok.Listener
86
+
87
+ override get initialState(): PortExposerState {
88
+ return {
89
+ ...super.initialState,
90
+ connected: false
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Expose the local port via ngrok.
96
+ *
97
+ * Creates an ngrok tunnel to the specified local port and returns
98
+ * the SSL-enabled public URL. Emits `exposed` on success or `error` on failure.
99
+ *
100
+ * @param port - Optional port override; falls back to `options.port`
101
+ * @returns Promise resolving to the public URL string
102
+ * @throws {Error} When no port is specified in options or parameter
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const exposer = container.feature('portExposer', { port: 3000 })
107
+ * const url = await exposer.expose()
108
+ * console.log(`Public URL: ${url}`)
109
+ *
110
+ * // Override port at call time
111
+ * const url2 = await exposer.expose(8080)
112
+ * ```
113
+ */
114
+ async expose(port?: number): Promise<string> {
115
+ const targetPort = port || this.options.port
116
+
117
+ if (!targetPort) {
118
+ throw new Error('Port must be specified either in options or as parameter')
119
+ }
120
+
121
+ try {
122
+ // Set up ngrok configuration
123
+ const config: any = {
124
+ addr: targetPort,
125
+ ...this.options.config
126
+ }
127
+
128
+ // Add optional configuration
129
+ if (this.options.authToken) {
130
+ config.authtoken = this.options.authToken
131
+ }
132
+ if (this.options.region) {
133
+ config.region = this.options.region
134
+ }
135
+ if (this.options.subdomain) {
136
+ config.subdomain = this.options.subdomain
137
+ }
138
+ if (this.options.domain) {
139
+ config.domain = this.options.domain
140
+ }
141
+ if (this.options.basicAuth) {
142
+ config.basic_auth = this.options.basicAuth
143
+ }
144
+ if (this.options.oauth) {
145
+ config.oauth = this.options.oauth
146
+ }
147
+
148
+ // Start ngrok listener
149
+ this.ngrokListener = await ngrok.forward(config)
150
+ const publicUrl = this.ngrokListener.url() || undefined
151
+
152
+ // Update state
153
+ this.setState({
154
+ connected: true,
155
+ publicUrl,
156
+ localPort: targetPort,
157
+ sessionInfo: {
158
+ authToken: this.options.authToken,
159
+ region: this.options.region,
160
+ subdomain: this.options.subdomain
161
+ },
162
+ connectedAt: new Date(),
163
+ lastError: undefined
164
+ })
165
+
166
+ this.emit('exposed', { publicUrl, localPort: targetPort })
167
+
168
+ return publicUrl!
169
+
170
+ } catch (error) {
171
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
172
+
173
+ this.setState({
174
+ connected: false,
175
+ lastError: errorMessage
176
+ })
177
+
178
+ this.emit('error', error)
179
+ throw error
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Stop exposing the port and close the ngrok tunnel.
185
+ *
186
+ * Tears down the ngrok listener, resets connection state, and emits `closed`.
187
+ * Safe to call when no tunnel is active (no-op).
188
+ *
189
+ * @returns Promise that resolves when the tunnel is fully closed
190
+ * @throws {Error} When the ngrok listener fails to close
191
+ *
192
+ * @example
193
+ * ```typescript
194
+ * const exposer = container.feature('portExposer', { port: 3000 })
195
+ * await exposer.expose()
196
+ * // ... later
197
+ * await exposer.close()
198
+ * console.log(exposer.isConnected()) // false
199
+ * ```
200
+ */
201
+ async close(): Promise<void> {
202
+ if (this.ngrokListener) {
203
+ try {
204
+ await this.ngrokListener.close()
205
+ this.ngrokListener = undefined
206
+
207
+ this.setState({
208
+ connected: false,
209
+ publicUrl: undefined,
210
+ localPort: undefined,
211
+ connectedAt: undefined
212
+ })
213
+
214
+ this.emit('closed')
215
+ } catch (error) {
216
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error'
217
+ this.setState({ lastError: errorMessage })
218
+ this.emit('error', error)
219
+ throw error
220
+ }
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Get the current public URL if connected.
226
+ *
227
+ * Returns the live URL from the ngrok listener, or `undefined` if no tunnel is active.
228
+ *
229
+ * @returns The public HTTPS URL string, or undefined when disconnected
230
+ *
231
+ * @example
232
+ * ```typescript
233
+ * const exposer = container.feature('portExposer', { port: 3000 })
234
+ * await exposer.expose()
235
+ * console.log(exposer.getPublicUrl()) // 'https://abc123.ngrok.io'
236
+ * ```
237
+ */
238
+ getPublicUrl(): string | undefined {
239
+ return this.ngrokListener?.url() || undefined
240
+ }
241
+
242
+ /**
243
+ * Check if the ngrok tunnel is currently connected.
244
+ *
245
+ * @returns `true` when an active tunnel exists, `false` otherwise
246
+ *
247
+ * @example
248
+ * ```typescript
249
+ * const exposer = container.feature('portExposer', { port: 3000 })
250
+ * console.log(exposer.isConnected()) // false
251
+ * await exposer.expose()
252
+ * console.log(exposer.isConnected()) // true
253
+ * ```
254
+ */
255
+ isConnected(): boolean {
256
+ return this.state.get('connected') ?? false
257
+ }
258
+
259
+ /**
260
+ * Get a snapshot of the current connection information.
261
+ *
262
+ * Returns an object with the tunnel's connected status, public URL,
263
+ * local port, connection timestamp, and session metadata.
264
+ *
265
+ * @returns An object containing `connected`, `publicUrl`, `localPort`, `connectedAt`, and `sessionInfo`
266
+ *
267
+ * @example
268
+ * ```typescript
269
+ * const exposer = container.feature('portExposer', { port: 3000 })
270
+ * await exposer.expose()
271
+ * const info = exposer.getConnectionInfo()
272
+ * console.log(info.publicUrl, info.localPort, info.connectedAt)
273
+ * ```
274
+ */
275
+ getConnectionInfo() {
276
+ const state = this.state.current
277
+ return {
278
+ connected: state.connected,
279
+ publicUrl: state.publicUrl,
280
+ localPort: state.localPort,
281
+ connectedAt: state.connectedAt,
282
+ sessionInfo: state.sessionInfo
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Close the existing tunnel and re-expose with optionally updated options.
288
+ *
289
+ * Calls `close()` first, merges any new options, then calls `expose()`.
290
+ *
291
+ * @param newOptions - Optional partial options to merge before reconnecting
292
+ * @returns Promise resolving to the new public URL string
293
+ *
294
+ * @example
295
+ * ```typescript
296
+ * const exposer = container.feature('portExposer', { port: 3000 })
297
+ * await exposer.expose()
298
+ * // Switch to a different port
299
+ * const newUrl = await exposer.reconnect({ port: 8080 })
300
+ * ```
301
+ */
302
+ async reconnect(newOptions?: Partial<PortExposerOptions>): Promise<string> {
303
+ await this.close()
304
+
305
+ if (newOptions) {
306
+ Object.assign(this.options, newOptions)
307
+ }
308
+
309
+ return this.expose()
310
+ }
311
+
312
+ /**
313
+ * Disable the feature, ensuring the ngrok tunnel is closed first.
314
+ *
315
+ * Overrides the base `disable()` to guarantee that the tunnel is
316
+ * torn down before the feature is marked as disabled.
317
+ *
318
+ * @returns This PortExposer instance
319
+ *
320
+ * @example
321
+ * ```typescript
322
+ * const exposer = container.feature('portExposer', { port: 3000 })
323
+ * await exposer.expose()
324
+ * await exposer.disable()
325
+ * ```
326
+ */
327
+ async disable(): Promise<this> {
328
+ await this.close()
329
+ return this
330
+ }
331
+ }
332
+
333
+ // Register the feature with the features registry
334
+ export default features.register('portExposer', PortExposer)
335
+
336
+ // Module augmentation for type safety
337
+ declare module '../../feature.js' {
338
+ interface AvailableFeatures {
339
+ portExposer: typeof PortExposer
340
+ }
341
+ }
342
+
@@ -0,0 +1,273 @@
1
+ import { z } from 'zod'
2
+ import { SQL } from 'bun'
3
+ import { Feature, features } from '../feature.js'
4
+ import { FeatureStateSchema, FeatureOptionsSchema } from '../../schemas/base.js'
5
+ import type { ContainerContext } from '../../container.js'
6
+
7
+ type SqlValue = string | number | boolean | bigint | Uint8Array | Buffer | null
8
+
9
+ export const PostgresStateSchema = FeatureStateSchema.extend({
10
+ connected: z.boolean().default(false).describe('Whether the postgres connection is currently open'),
11
+ url: z.string().default('').describe('Connection URL used for this postgres feature instance'),
12
+ lastQuery: z.string().optional().describe('Most recent SQL query string that was executed'),
13
+ lastRowCount: z.number().optional().describe('Row count returned by the most recent query execution'),
14
+ lastError: z.string().optional().describe('Most recent postgres error message, if any'),
15
+ })
16
+
17
+ export const PostgresOptionsSchema = FeatureOptionsSchema.extend({
18
+ url: z.string().min(1).optional().describe('Postgres connection URL, e.g. postgres://user:pass@host:5432/db'),
19
+ })
20
+
21
+ export type PostgresState = z.infer<typeof PostgresStateSchema>
22
+ export type PostgresOptions = z.infer<typeof PostgresOptionsSchema>
23
+
24
+ /**
25
+ * Postgres feature for safe SQL execution through Bun's native SQL client.
26
+ *
27
+ * Supports:
28
+ * - parameterized query execution (`query` / `execute`)
29
+ * - tagged-template query execution (`sql`) to avoid manual placeholder wiring
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * const postgres = container.feature('postgres', { url: process.env.DATABASE_URL! })
34
+ *
35
+ * const users = await postgres.query<{ id: number; email: string }>(
36
+ * 'select id, email from users where id = $1',
37
+ * [123]
38
+ * )
39
+ *
40
+ * const rows = await postgres.sql<{ id: number }>`
41
+ * select id from users where email = ${'hello@example.com'}
42
+ * `
43
+ * ```
44
+ */
45
+ export class Postgres extends Feature<PostgresState, PostgresOptions> {
46
+ static override shortcut = 'features.postgres' as const
47
+ static override stateSchema = PostgresStateSchema
48
+ static override optionsSchema = PostgresOptionsSchema
49
+
50
+ private _client: SQL
51
+
52
+ /**
53
+ * Default state for the Postgres feature before a connection is established.
54
+ * @returns The initial PostgresState with `connected: false` and empty `url`
55
+ */
56
+ override get initialState(): PostgresState {
57
+ return {
58
+ enabled: false,
59
+ connected: false,
60
+ url: '',
61
+ }
62
+ }
63
+
64
+ constructor(options: PostgresOptions, context: ContainerContext) {
65
+ super(options, context)
66
+
67
+ if (!options.url) {
68
+ throw new Error('Postgres feature requires options.url')
69
+ }
70
+
71
+ this._client = new SQL(options.url)
72
+ this.hide('_client')
73
+
74
+ this.setState({
75
+ connected: true,
76
+ url: options.url,
77
+ })
78
+ }
79
+
80
+ /**
81
+ * Returns the underlying Bun SQL postgres client.
82
+ * @returns The raw `SQL` instance used for all database operations
83
+ */
84
+ get client() {
85
+ return this._client
86
+ }
87
+
88
+ /**
89
+ * Executes a SELECT-like query and returns result rows.
90
+ *
91
+ * Use postgres placeholders (`$1`, `$2`, ...) for `params`.
92
+ *
93
+ * @param queryText - The SQL query string with optional `$N` placeholders
94
+ * @param params - Ordered array of values to bind to the placeholders
95
+ * @returns Promise resolving to an array of typed result rows
96
+ * @throws {Error} When query text is empty or params contain `undefined`
97
+ *
98
+ * @example
99
+ * ```typescript
100
+ * const pg = container.feature('postgres', { url: process.env.DATABASE_URL! })
101
+ * const users = await pg.query<{ id: number; email: string }>(
102
+ * 'SELECT id, email FROM users WHERE active = $1',
103
+ * [true]
104
+ * )
105
+ * ```
106
+ */
107
+ async query<T extends object = Record<string, unknown>>(queryText: string, params: SqlValue[] = []): Promise<T[]> {
108
+ assertQueryText(queryText)
109
+ assertParams(params)
110
+
111
+ try {
112
+ const result = await this.client.unsafe(queryText, params)
113
+ const rows = Array.isArray(result) ? result as T[] : []
114
+ const rowCount = resolveRowCount(result)
115
+
116
+ this.setState({
117
+ lastQuery: queryText,
118
+ lastRowCount: rowCount,
119
+ lastError: undefined,
120
+ })
121
+
122
+ this.emit('query', queryText, params, rowCount)
123
+ return rows
124
+ } catch (error: any) {
125
+ this.setState({
126
+ lastQuery: queryText,
127
+ lastError: error?.message || String(error),
128
+ })
129
+
130
+ this.emit('error', error)
131
+ throw error
132
+ }
133
+ }
134
+
135
+ /**
136
+ * Executes a write/update/delete statement and returns metadata.
137
+ *
138
+ * Use postgres placeholders (`$1`, `$2`, ...) for `params`.
139
+ *
140
+ * @param queryText - The SQL statement string with optional `$N` placeholders
141
+ * @param params - Ordered array of values to bind to the placeholders
142
+ * @returns Promise resolving to `{ rowCount }` indicating affected rows
143
+ * @throws {Error} When query text is empty or params contain `undefined`
144
+ *
145
+ * @example
146
+ * ```typescript
147
+ * const pg = container.feature('postgres', { url: process.env.DATABASE_URL! })
148
+ * const { rowCount } = await pg.execute(
149
+ * 'UPDATE users SET active = $1 WHERE last_login < $2',
150
+ * [false, '2024-01-01']
151
+ * )
152
+ * console.log(`Deactivated ${rowCount} users`)
153
+ * ```
154
+ */
155
+ async execute(queryText: string, params: SqlValue[] = []): Promise<{ rowCount: number }> {
156
+ assertQueryText(queryText)
157
+ assertParams(params)
158
+
159
+ try {
160
+ const result = await this.client.unsafe(queryText, params)
161
+ const rowCount = resolveRowCount(result)
162
+
163
+ this.setState({
164
+ lastQuery: queryText,
165
+ lastRowCount: rowCount,
166
+ lastError: undefined,
167
+ })
168
+
169
+ this.emit('execute', queryText, params, rowCount)
170
+ return { rowCount }
171
+ } catch (error: any) {
172
+ this.setState({
173
+ lastQuery: queryText,
174
+ lastError: error?.message || String(error),
175
+ })
176
+
177
+ this.emit('error', error)
178
+ throw error
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Safe tagged-template SQL helper.
184
+ *
185
+ * Values become bound parameters automatically, preventing SQL injection.
186
+ *
187
+ * @param strings - Template literal string segments
188
+ * @param values - Interpolated values that become bound `$N` parameters
189
+ * @returns Promise resolving to an array of typed result rows
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * const pg = container.feature('postgres', { url: process.env.DATABASE_URL! })
194
+ * const email = 'hello@example.com'
195
+ * const rows = await pg.sql<{ id: number }>`
196
+ * SELECT id FROM users WHERE email = ${email}
197
+ * `
198
+ * ```
199
+ */
200
+ async sql<T extends object = Record<string, unknown>>(strings: TemplateStringsArray, ...values: SqlValue[]): Promise<T[]> {
201
+ const built = buildDollarQuery(strings, values)
202
+ return this.query<T>(built.text, built.params)
203
+ }
204
+
205
+ /**
206
+ * Closes the postgres connection and updates feature state.
207
+ *
208
+ * Emits `closed` after the connection is torn down.
209
+ *
210
+ * @returns This Postgres feature instance for method chaining
211
+ *
212
+ * @example
213
+ * ```typescript
214
+ * const pg = container.feature('postgres', { url: process.env.DATABASE_URL! })
215
+ * // ... run queries ...
216
+ * await pg.close()
217
+ * ```
218
+ */
219
+ async close() {
220
+ await this.client.close()
221
+ this.setState({ connected: false })
222
+ this.emit('closed')
223
+ return this
224
+ }
225
+ }
226
+
227
+ export default features.register('postgres', Postgres)
228
+
229
+ declare module '../../feature.js' {
230
+ interface AvailableFeatures {
231
+ postgres: typeof Postgres
232
+ }
233
+ }
234
+
235
+ function assertQueryText(queryText: string) {
236
+ if (typeof queryText !== 'string' || queryText.trim().length === 0) {
237
+ throw new Error('SQL query text must be a non-empty string')
238
+ }
239
+ }
240
+
241
+ function assertParams(params: SqlValue[]) {
242
+ if (!Array.isArray(params)) {
243
+ throw new Error('SQL params must be an array')
244
+ }
245
+
246
+ if (params.some((param) => param === undefined)) {
247
+ throw new Error('SQL params cannot contain undefined values. Use null instead.')
248
+ }
249
+ }
250
+
251
+ function buildDollarQuery(strings: TemplateStringsArray, values: SqlValue[]) {
252
+ if (strings.length !== values.length + 1) {
253
+ throw new Error('Invalid SQL template literal input')
254
+ }
255
+
256
+ const chunks: string[] = []
257
+
258
+ for (let i = 0; i < strings.length; i++) {
259
+ chunks.push(strings[i]!)
260
+ if (i < values.length) {
261
+ chunks.push(`$${i + 1}`)
262
+ }
263
+ }
264
+
265
+ return { text: chunks.join(''), params: values }
266
+ }
267
+
268
+ function resolveRowCount(result: any): number {
269
+ if (typeof result?.count === 'number') return result.count
270
+ if (typeof result?.rowCount === 'number') return result.rowCount
271
+ if (Array.isArray(result)) return result.length
272
+ return 0
273
+ }