khal-os 1.260324.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 (408) hide show
  1. package/.env.example +23 -0
  2. package/.genie/mailbox/cli-sent.jsonl +3 -0
  3. package/.genie/mailbox/ds1-wave2-engineer-1.json +15 -0
  4. package/.genie/mailbox/ds1-wave2-engineer-2.json +15 -0
  5. package/.genie/mailbox/ds1-wave2-engineer-3.json +15 -0
  6. package/.genie/state/os-observability.json +39 -0
  7. package/.genie/state/tmux-control-mode-terminal.json +28 -0
  8. package/.genie/wishes/genieos-one-theme/WISH.md +417 -0
  9. package/.genie/wishes/workos-prod-rbac/WISH.md +345 -0
  10. package/.github/workflows/ci.yml +39 -0
  11. package/.github/workflows/release.yml +78 -0
  12. package/.github/workflows/version.yml +122 -0
  13. package/.husky/pre-commit +1 -0
  14. package/.pnpm-approve-builds.json +1 -0
  15. package/CLAUDE.md +117 -0
  16. package/LICENSE +21 -0
  17. package/README.md +38 -0
  18. package/biome.json +124 -0
  19. package/bun.lock +1249 -0
  20. package/docs/workos-setup.md +116 -0
  21. package/ecosystem.config.cjs +26 -0
  22. package/instrumentation.ts +8 -0
  23. package/knip.json +35 -0
  24. package/nats.conf +7 -0
  25. package/next.config.ts +25 -0
  26. package/package.json +78 -0
  27. package/packages/dev3000-app/components.ts +12 -0
  28. package/packages/dev3000-app/manifest.ts +19 -0
  29. package/packages/dev3000-app/package.json +23 -0
  30. package/packages/dev3000-app/views/dev3000/Dev3000App.tsx +758 -0
  31. package/packages/dev3000-app/views/dev3000/ErrorsPanel.tsx +160 -0
  32. package/packages/dev3000-app/views/dev3000/dev3000-context.tsx +21 -0
  33. package/packages/dev3000-app/views/dev3000/index.ts +4 -0
  34. package/packages/dev3000-app/views/dev3000/schema.ts +55 -0
  35. package/packages/dev3000-app/views/dev3000/service/index.ts +358 -0
  36. package/packages/dev3000-app/views/dev3000/service/runtime +1 -0
  37. package/packages/dev3000-app/views/dev3000/subjects.ts +9 -0
  38. package/packages/dev3000-app/views/dev3000/types.ts +77 -0
  39. package/packages/files-app/components.ts +12 -0
  40. package/packages/files-app/manifest.ts +19 -0
  41. package/packages/files-app/package.json +23 -0
  42. package/packages/files-app/views/files/ContextMenu.tsx +151 -0
  43. package/packages/files-app/views/files/DeleteConfirmDialog.tsx +39 -0
  44. package/packages/files-app/views/files/FileItem.tsx +128 -0
  45. package/packages/files-app/views/files/FilesApp.tsx +509 -0
  46. package/packages/files-app/views/files/FilesListView.tsx +201 -0
  47. package/packages/files-app/views/files/FilesToolbar.tsx +117 -0
  48. package/packages/files-app/views/files/GridView.tsx +90 -0
  49. package/packages/files-app/views/files/InlineInput.tsx +131 -0
  50. package/packages/files-app/views/files/UploadOverlay.tsx +61 -0
  51. package/packages/files-app/views/files/schema.ts +49 -0
  52. package/packages/files-app/views/files/service/index.ts +184 -0
  53. package/packages/files-app/views/files/service/runtime +1 -0
  54. package/packages/files-app/views/files/use-files.ts +201 -0
  55. package/packages/files-app/views/files/use-upload.ts +105 -0
  56. package/packages/genie-app/components.ts +12 -0
  57. package/packages/genie-app/lib/subjects.ts +87 -0
  58. package/packages/genie-app/manifest.ts +19 -0
  59. package/packages/genie-app/package.json +29 -0
  60. package/packages/genie-app/views/genie/service/agent-lifecycle.ts +136 -0
  61. package/packages/genie-app/views/genie/service/cli.ts +114 -0
  62. package/packages/genie-app/views/genie/service/comms.ts +141 -0
  63. package/packages/genie-app/views/genie/service/directory.ts +167 -0
  64. package/packages/genie-app/views/genie/service/index.ts +219 -0
  65. package/packages/genie-app/views/genie/service/system.ts +123 -0
  66. package/packages/genie-app/views/genie/service/teams.ts +191 -0
  67. package/packages/genie-app/views/genie/service/terminal-proxy.ts +184 -0
  68. package/packages/genie-app/views/genie/service/tmux-control.ts +318 -0
  69. package/packages/genie-app/views/genie/service/wishes.ts +270 -0
  70. package/packages/genie-app/views/genie/ui/GenieApp.tsx +5 -0
  71. package/packages/genie-app/views/genie/ui/PaneCard.tsx +307 -0
  72. package/packages/genie-app/views/genie/ui/Sidebar.tsx +212 -0
  73. package/packages/genie-app/views/genie/ui/TabBar.tsx +70 -0
  74. package/packages/genie-app/views/genie/ui/WorkspaceCanvas.tsx +343 -0
  75. package/packages/genie-app/views/genie/ui/XTermPane.tsx +306 -0
  76. package/packages/genie-app/views/genie/ui/hooks/useNatsAction.ts +54 -0
  77. package/packages/genie-app/views/genie/ui/hooks/useNatsRequest.ts +68 -0
  78. package/packages/genie-app/views/genie/ui/panels/AgentsPanel.tsx +399 -0
  79. package/packages/genie-app/views/genie/ui/panels/ChatPanel.tsx +351 -0
  80. package/packages/genie-app/views/genie/ui/panels/SystemPanel.tsx +195 -0
  81. package/packages/genie-app/views/genie/ui/panels/TeamsPanel.tsx +560 -0
  82. package/packages/genie-app/views/genie/ui/panels/WishesPanel.tsx +424 -0
  83. package/packages/nats-viewer-app/components.ts +12 -0
  84. package/packages/nats-viewer-app/manifest.ts +18 -0
  85. package/packages/nats-viewer-app/package.json +14 -0
  86. package/packages/nats-viewer-app/views/nats-viewer/ActiveSubs.tsx +34 -0
  87. package/packages/nats-viewer-app/views/nats-viewer/MessageLog.tsx +247 -0
  88. package/packages/nats-viewer-app/views/nats-viewer/NatsViewer.tsx +209 -0
  89. package/packages/nats-viewer-app/views/nats-viewer/PublishPanel.tsx +111 -0
  90. package/packages/nats-viewer-app/views/nats-viewer/RequestPanel.tsx +165 -0
  91. package/packages/nats-viewer-app/views/nats-viewer/Sidebar.tsx +59 -0
  92. package/packages/nats-viewer-app/views/nats-viewer/SubjectCatalog.tsx +63 -0
  93. package/packages/nats-viewer-app/views/nats-viewer/SubscribeInput.tsx +59 -0
  94. package/packages/nats-viewer-app/views/nats-viewer/index.ts +5 -0
  95. package/packages/nats-viewer-app/views/nats-viewer/nats-viewer-context.tsx +31 -0
  96. package/packages/nats-viewer-app/views/nats-viewer/types.ts +7 -0
  97. package/packages/nats-viewer-app/views/nats-viewer/use-message-buffer.ts +55 -0
  98. package/packages/os-cli/package.json +18 -0
  99. package/packages/os-cli/src/commands/events.ts +176 -0
  100. package/packages/os-cli/src/commands/logs.ts +96 -0
  101. package/packages/os-cli/src/commands/status.ts +53 -0
  102. package/packages/os-cli/src/commands/traces.ts +115 -0
  103. package/packages/os-cli/src/index.ts +15 -0
  104. package/packages/os-cli/src/lib/formatter.ts +123 -0
  105. package/packages/os-cli/src/lib/nats.ts +16 -0
  106. package/packages/os-cli/src/lib/trace-tree.ts +144 -0
  107. package/packages/os-cli/tsconfig.json +12 -0
  108. package/packages/os-sdk/package.json +27 -0
  109. package/packages/os-sdk/src/api/handler.ts +67 -0
  110. package/packages/os-sdk/src/config.ts +68 -0
  111. package/packages/os-sdk/src/db/factory.test.ts +42 -0
  112. package/packages/os-sdk/src/db/factory.ts +72 -0
  113. package/packages/os-sdk/src/db/migrate.ts +140 -0
  114. package/packages/os-sdk/src/db/provision.ts +44 -0
  115. package/packages/os-sdk/src/index.ts +36 -0
  116. package/packages/os-sdk/src/service/console-intercept.ts +60 -0
  117. package/packages/os-sdk/src/service/logger.ts +88 -0
  118. package/packages/os-sdk/src/service/o11y-streams.ts +88 -0
  119. package/packages/os-sdk/src/service/runtime.ts +259 -0
  120. package/packages/os-sdk/src/service/trace.ts +71 -0
  121. package/packages/os-sdk/tsconfig.json +16 -0
  122. package/packages/os-ui/package.json +13 -0
  123. package/packages/os-ui/src/index.ts +29 -0
  124. package/packages/os-ui/src/server.ts +4 -0
  125. package/packages/os-ui/tsconfig.json +19 -0
  126. package/packages/settings-app/components.ts +12 -0
  127. package/packages/settings-app/manifest.ts +18 -0
  128. package/packages/settings-app/package.json +14 -0
  129. package/packages/settings-app/views/settings/Settings.tsx +492 -0
  130. package/packages/terminal-app/components.ts +12 -0
  131. package/packages/terminal-app/manifest.ts +20 -0
  132. package/packages/terminal-app/package.json +23 -0
  133. package/packages/terminal-app/views/terminal/schema.ts +82 -0
  134. package/packages/terminal-app/views/terminal/service/index.ts +133 -0
  135. package/packages/terminal-app/views/terminal/service/runtime +1 -0
  136. package/packages/terminal-app/views/terminal/service/session.ts +290 -0
  137. package/packages/terminal-app/views/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
  138. package/packages/terminal-app/views/terminal/types.ts +26 -0
  139. package/packages/terminal-app/views/terminal/ui/MultiTerminalApp.tsx +615 -0
  140. package/packages/terminal-app/views/terminal/ui/SplitDragHandle.tsx +91 -0
  141. package/packages/terminal-app/views/terminal/ui/SplitPaneRenderer.tsx +112 -0
  142. package/packages/terminal-app/views/terminal/ui/TerminalPane.tsx +478 -0
  143. package/packages/terminal-app/views/terminal/ui/TerminalTabBar.tsx +131 -0
  144. package/pnpm-workspace.yaml +9 -0
  145. package/postcss.config.mjs +7 -0
  146. package/public/file.svg +1 -0
  147. package/public/globe.svg +1 -0
  148. package/public/icons/code-server.svg +6 -0
  149. package/public/icons/default.svg +5 -0
  150. package/public/icons/dusk/1password.svg +1 -0
  151. package/public/icons/dusk/activity_monitor.svg +1 -0
  152. package/public/icons/dusk/app_store.svg +1 -0
  153. package/public/icons/dusk/atom.svg +1 -0
  154. package/public/icons/dusk/brave.svg +1 -0
  155. package/public/icons/dusk/calculator.svg +1 -0
  156. package/public/icons/dusk/calendar.svg +1 -0
  157. package/public/icons/dusk/chrome.svg +1 -0
  158. package/public/icons/dusk/chrome2.svg +1 -0
  159. package/public/icons/dusk/dashboard.svg +13 -0
  160. package/public/icons/dusk/discord.svg +1 -0
  161. package/public/icons/dusk/dropbox.svg +1 -0
  162. package/public/icons/dusk/electron.svg +1 -0
  163. package/public/icons/dusk/figma.svg +1 -0
  164. package/public/icons/dusk/finder.svg +1 -0
  165. package/public/icons/dusk/finder2.svg +1 -0
  166. package/public/icons/dusk/finder3.svg +1 -0
  167. package/public/icons/dusk/firefox.svg +1 -0
  168. package/public/icons/dusk/framer.svg +1 -0
  169. package/public/icons/dusk/gimp.svg +1 -0
  170. package/public/icons/dusk/github_desktop.svg +1 -0
  171. package/public/icons/dusk/hyper.svg +1 -0
  172. package/public/icons/dusk/hyper3.svg +1 -0
  173. package/public/icons/dusk/intellij.svg +1 -0
  174. package/public/icons/dusk/iterm2.svg +1 -0
  175. package/public/icons/dusk/itunes.svg +1 -0
  176. package/public/icons/dusk/mail.svg +1 -0
  177. package/public/icons/dusk/messenger.svg +1 -0
  178. package/public/icons/dusk/mongodb.svg +1 -0
  179. package/public/icons/dusk/notes.svg +1 -0
  180. package/public/icons/dusk/notion.svg +1 -0
  181. package/public/icons/dusk/obs.svg +1 -0
  182. package/public/icons/dusk/pages.svg +1 -0
  183. package/public/icons/dusk/photos.svg +1 -0
  184. package/public/icons/dusk/postman.svg +1 -0
  185. package/public/icons/dusk/preview.svg +1 -0
  186. package/public/icons/dusk/reminders.svg +1 -0
  187. package/public/icons/dusk/safari.svg +1 -0
  188. package/public/icons/dusk/sequel_pro.svg +1 -0
  189. package/public/icons/dusk/sketch.svg +1 -0
  190. package/public/icons/dusk/skype.svg +1 -0
  191. package/public/icons/dusk/slack.svg +1 -0
  192. package/public/icons/dusk/slack2.svg +1 -0
  193. package/public/icons/dusk/spotify.svg +1 -0
  194. package/public/icons/dusk/steam.svg +1 -0
  195. package/public/icons/dusk/system_preferences.svg +1 -0
  196. package/public/icons/dusk/tableplus.svg +1 -0
  197. package/public/icons/dusk/teams.svg +1 -0
  198. package/public/icons/dusk/telegram.svg +1 -0
  199. package/public/icons/dusk/terminal.svg +1 -0
  200. package/public/icons/dusk/todoist.svg +1 -0
  201. package/public/icons/dusk/trash.svg +1 -0
  202. package/public/icons/dusk/trello.svg +1 -0
  203. package/public/icons/dusk/vivaldi.svg +1 -0
  204. package/public/icons/dusk/vlc.svg +1 -0
  205. package/public/icons/dusk/vscode.svg +1 -0
  206. package/public/icons/dusk/whatsapp.svg +1 -0
  207. package/public/icons/dusk/xeyes.svg +1 -0
  208. package/public/icons/dusk/zoom.svg +1 -0
  209. package/public/icons/files.svg +5 -0
  210. package/public/icons/pwa/icon-192.png +0 -0
  211. package/public/icons/pwa/icon-512.png +0 -0
  212. package/public/icons/settings.svg +14 -0
  213. package/public/icons/terminal.svg +5 -0
  214. package/public/icons/text-editor.svg +7 -0
  215. package/public/manifest.json +38 -0
  216. package/public/next.svg +1 -0
  217. package/public/sw.js +41 -0
  218. package/public/vercel.svg +1 -0
  219. package/public/wallpapers/default.svg +10 -0
  220. package/public/window.svg +1 -0
  221. package/scripts/generate-pwa-icons.mjs +33 -0
  222. package/scripts/install-nats.sh +37 -0
  223. package/sentry.client.config.ts +21 -0
  224. package/sentry.edge.config.ts +12 -0
  225. package/sentry.server.config.ts +12 -0
  226. package/src/app/api/files/download/route.ts +81 -0
  227. package/src/app/api/files/download-zip/route.ts +102 -0
  228. package/src/app/api/files/upload/route.ts +58 -0
  229. package/src/app/api/webhooks/workos/route.ts +98 -0
  230. package/src/app/auth/callback/route.ts +16 -0
  231. package/src/app/auth/logout/route.ts +15 -0
  232. package/src/app/desktop/desktop-shell.tsx +110 -0
  233. package/src/app/desktop/layout.tsx +8 -0
  234. package/src/app/desktop/page.tsx +24 -0
  235. package/src/app/favicon.ico +0 -0
  236. package/src/app/globals.css +7 -0
  237. package/src/app/layout.tsx +64 -0
  238. package/src/app/offline/page.tsx +83 -0
  239. package/src/app/page.tsx +5 -0
  240. package/src/app/standalone/[appId]/page.tsx +28 -0
  241. package/src/app/standalone/layout.tsx +10 -0
  242. package/src/components/app-icon.tsx +55 -0
  243. package/src/components/apps/_echo/schema.ts +14 -0
  244. package/src/components/apps/_echo/service/index.ts +42 -0
  245. package/src/components/apps/app-manifest.ts +97 -0
  246. package/src/components/apps/app-registry.ts +55 -0
  247. package/src/components/apps/dev3000/Dev3000App.tsx +224 -0
  248. package/src/components/apps/dev3000/ErrorsPanel.tsx +160 -0
  249. package/src/components/apps/dev3000/Sidebar.tsx +41 -0
  250. package/src/components/apps/dev3000/TimelineLog.tsx +173 -0
  251. package/src/components/apps/dev3000/dev3000-context.tsx +29 -0
  252. package/src/components/apps/dev3000/index.ts +4 -0
  253. package/src/components/apps/dev3000/schema.ts +48 -0
  254. package/src/components/apps/dev3000/service/index.ts +520 -0
  255. package/src/components/apps/dev3000/service/runtime +1 -0
  256. package/src/components/apps/dev3000/types.ts +15 -0
  257. package/src/components/apps/dev3000/use-message-buffer.ts +46 -0
  258. package/src/components/apps/files/ContextMenu.tsx +151 -0
  259. package/src/components/apps/files/DeleteConfirmDialog.tsx +78 -0
  260. package/src/components/apps/files/FileItem.tsx +128 -0
  261. package/src/components/apps/files/FilesApp.tsx +509 -0
  262. package/src/components/apps/files/FilesListView.tsx +201 -0
  263. package/src/components/apps/files/FilesToolbar.tsx +117 -0
  264. package/src/components/apps/files/GridView.tsx +90 -0
  265. package/src/components/apps/files/InlineInput.tsx +131 -0
  266. package/src/components/apps/files/UploadOverlay.tsx +61 -0
  267. package/src/components/apps/files/schema.ts +49 -0
  268. package/src/components/apps/files/service/index.ts +227 -0
  269. package/src/components/apps/files/service/runtime +1 -0
  270. package/src/components/apps/files/use-files.ts +201 -0
  271. package/src/components/apps/files/use-upload.ts +105 -0
  272. package/src/components/apps/nats-viewer/ActiveSubs.tsx +34 -0
  273. package/src/components/apps/nats-viewer/MessageLog.tsx +247 -0
  274. package/src/components/apps/nats-viewer/NatsViewer.tsx +209 -0
  275. package/src/components/apps/nats-viewer/PublishPanel.tsx +113 -0
  276. package/src/components/apps/nats-viewer/RequestPanel.tsx +167 -0
  277. package/src/components/apps/nats-viewer/Sidebar.tsx +62 -0
  278. package/src/components/apps/nats-viewer/SubjectCatalog.tsx +64 -0
  279. package/src/components/apps/nats-viewer/SubscribeInput.tsx +59 -0
  280. package/src/components/apps/nats-viewer/index.ts +5 -0
  281. package/src/components/apps/nats-viewer/nats-viewer-context.tsx +31 -0
  282. package/src/components/apps/nats-viewer/types.ts +7 -0
  283. package/src/components/apps/nats-viewer/use-message-buffer.ts +55 -0
  284. package/src/components/apps/settings/Settings.tsx +492 -0
  285. package/src/components/apps/terminal/schema.ts +82 -0
  286. package/src/components/apps/terminal/service/index.ts +189 -0
  287. package/src/components/apps/terminal/service/runtime +1 -0
  288. package/src/components/apps/terminal/service/session.ts +296 -0
  289. package/src/components/apps/terminal/service/shell-hooks/bashrc-hook.sh +21 -0
  290. package/src/components/apps/terminal/types.ts +26 -0
  291. package/src/components/apps/terminal/ui/MultiTerminalApp.tsx +617 -0
  292. package/src/components/apps/terminal/ui/SplitDragHandle.tsx +91 -0
  293. package/src/components/apps/terminal/ui/SplitPaneRenderer.tsx +112 -0
  294. package/src/components/apps/terminal/ui/TerminalPane.tsx +476 -0
  295. package/src/components/apps/terminal/ui/TerminalTabBar.tsx +131 -0
  296. package/src/components/desktop/AnimatedBackground.tsx +69 -0
  297. package/src/components/desktop/Desktop.tsx +79 -0
  298. package/src/components/desktop/DesktopBackground.tsx +16 -0
  299. package/src/components/desktop/DesktopIcon.tsx +49 -0
  300. package/src/components/desktop/ShortcutViewer.tsx +136 -0
  301. package/src/components/desktop/WindowRenderer.tsx +34 -0
  302. package/src/components/desktop/WindowSwitcher.tsx +42 -0
  303. package/src/components/notifications/NotificationCenter.tsx +153 -0
  304. package/src/components/notifications/NotificationToasts.tsx +66 -0
  305. package/src/components/notifications/OrphanSessionToast.tsx +293 -0
  306. package/src/components/os-primitives/collapsible-sidebar.tsx +226 -0
  307. package/src/components/os-primitives/dialog.tsx +76 -0
  308. package/src/components/os-primitives/empty-state.tsx +43 -0
  309. package/src/components/os-primitives/index.ts +21 -0
  310. package/src/components/os-primitives/list-view.tsx +155 -0
  311. package/src/components/os-primitives/property-panel.tsx +108 -0
  312. package/src/components/os-primitives/section-header.tsx +19 -0
  313. package/src/components/os-primitives/sidebar-nav.tsx +110 -0
  314. package/src/components/os-primitives/split-pane.tsx +146 -0
  315. package/src/components/os-primitives/status-badge.tsx +10 -0
  316. package/src/components/os-primitives/status-bar.tsx +100 -0
  317. package/src/components/os-primitives/toolbar.tsx +152 -0
  318. package/src/components/taskbar/AppLauncher.tsx +114 -0
  319. package/src/components/taskbar/RunningApps.tsx +71 -0
  320. package/src/components/taskbar/SystemTray.tsx +134 -0
  321. package/src/components/taskbar/Taskbar.tsx +45 -0
  322. package/src/components/taskbar/UserMenu.tsx +138 -0
  323. package/src/components/taskbar/WorkspaceSwitcher.tsx +9 -0
  324. package/src/components/ui/ContextMenu.tsx +130 -0
  325. package/src/components/ui/badge.tsx +39 -0
  326. package/src/components/ui/button.tsx +102 -0
  327. package/src/components/ui/command.tsx +165 -0
  328. package/src/components/ui/dropdown-menu.tsx +233 -0
  329. package/src/components/ui/input.tsx +48 -0
  330. package/src/components/ui/note.tsx +55 -0
  331. package/src/components/ui/separator.tsx +25 -0
  332. package/src/components/ui/spinner.tsx +42 -0
  333. package/src/components/ui/switch.tsx +36 -0
  334. package/src/components/ui/theme-provider.tsx +24 -0
  335. package/src/components/ui/theme-switcher.tsx +51 -0
  336. package/src/components/ui/tooltip.tsx +62 -0
  337. package/src/components/window/MobileWindowStack.tsx +218 -0
  338. package/src/components/window/SnapPreview.tsx +37 -0
  339. package/src/components/window/StandaloneFrame.tsx +170 -0
  340. package/src/components/window/Window.tsx +423 -0
  341. package/src/components/window/WindowContent.tsx +14 -0
  342. package/src/components/window/WindowControlsOverlay.tsx +89 -0
  343. package/src/components/window/WindowFrame.tsx +124 -0
  344. package/src/lib/auth/index.ts +27 -0
  345. package/src/lib/auth/roles.ts +50 -0
  346. package/src/lib/auth/types.ts +32 -0
  347. package/src/lib/auth/use-auth.ts +53 -0
  348. package/src/lib/auth/webhook-handler.ts +87 -0
  349. package/src/lib/auth/workos.ts +67 -0
  350. package/src/lib/constants.ts +1 -0
  351. package/src/lib/desktop/dedup.ts +57 -0
  352. package/src/lib/desktop/schema.ts +55 -0
  353. package/src/lib/files/filename-validation.ts +41 -0
  354. package/src/lib/files/safe-path.ts +49 -0
  355. package/src/lib/hooks/use-desktop-nats.ts +438 -0
  356. package/src/lib/hooks/use-is-mobile.ts +23 -0
  357. package/src/lib/hooks/use-launch-app.ts +79 -0
  358. package/src/lib/hooks/use-nats-notifications.ts +84 -0
  359. package/src/lib/hooks/use-nats.ts +60 -0
  360. package/src/lib/hooks/use-visual-viewport.ts +72 -0
  361. package/src/lib/icons/resolve-window-icon.ts +10 -0
  362. package/src/lib/keyboard/defaults.ts +146 -0
  363. package/src/lib/keyboard/types.ts +52 -0
  364. package/src/lib/keyboard/use-global-keybinds.ts +231 -0
  365. package/src/lib/nats-client.ts +255 -0
  366. package/src/lib/nats.ts +35 -0
  367. package/src/lib/notifications/schema.ts +12 -0
  368. package/src/lib/service-loader.ts +171 -0
  369. package/src/lib/subjects.ts +64 -0
  370. package/src/lib/utils.ts +6 -0
  371. package/src/lib/ws-bridge.ts +288 -0
  372. package/src/lib/ws-protocol.ts +53 -0
  373. package/src/lib/ws-server.ts +167 -0
  374. package/src/middleware.ts +57 -0
  375. package/src/stores/desktop-store.ts +112 -0
  376. package/src/stores/keybind-store.ts +66 -0
  377. package/src/stores/notification-store.ts +271 -0
  378. package/src/stores/theme-store.ts +25 -0
  379. package/src/stores/window-store.ts +294 -0
  380. package/src/theme/animations.css +68 -0
  381. package/src/theme/base.css +123 -0
  382. package/src/theme/controls.css +35 -0
  383. package/src/theme/design-tokens.css +276 -0
  384. package/src/theme/index.css +23 -0
  385. package/src/theme/menus.css +45 -0
  386. package/src/theme/status.css +41 -0
  387. package/src/theme/surfaces.css +94 -0
  388. package/src/theme/tailwind-map.css +138 -0
  389. package/src/theme/taskbar.css +25 -0
  390. package/src/theme/terminal.css +55 -0
  391. package/src/theme/typography.css +26 -0
  392. package/src/theme/utilities.css +156 -0
  393. package/src/theme/window.css +103 -0
  394. package/src/types/desktop-entry.ts +12 -0
  395. package/src/types/use-descendants.d.ts +13 -0
  396. package/src/types/window.ts +28 -0
  397. package/src/types.d.ts +9 -0
  398. package/tauri/Cargo.lock +5464 -0
  399. package/tauri/Cargo.toml +19 -0
  400. package/tauri/build.rs +3 -0
  401. package/tauri/capabilities/default.json +36 -0
  402. package/tauri/icons/128x128.png +0 -0
  403. package/tauri/icons/128x128@2x.png +0 -0
  404. package/tauri/icons/32x32.png +0 -0
  405. package/tauri/icons/icon.png +0 -0
  406. package/tauri/src/main.rs +396 -0
  407. package/tauri/tauri.conf.json +23 -0
  408. package/tsconfig.json +43 -0
@@ -0,0 +1,96 @@
1
+ import { DeliverPolicy, jetstream } from '@nats-io/jetstream';
2
+ import { Command } from 'commander';
3
+ import { formatLogEntry, meetsLevel } from '../lib/formatter.js';
4
+ import { connectNats, decode } from '../lib/nats.js';
5
+
6
+ /** Parse a duration string like "5m", "1h", "30s" into milliseconds. */
7
+ function parseDuration(input: string): number {
8
+ const match = input.match(/^(\d+)(s|m|h|d)$/);
9
+ if (!match) throw new Error(`Invalid duration: "${input}". Use format like 5m, 1h, 30s`);
10
+ const value = Number.parseInt(match[1], 10);
11
+ const unit = match[2];
12
+ const multipliers: Record<string, number> = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
13
+ return value * multipliers[unit];
14
+ }
15
+
16
+ export const logsCommand = new Command('logs')
17
+ .description('Tail structured logs from services')
18
+ .argument('[service]', 'Service name to tail logs for')
19
+ .option('--all', 'Tail logs from all services')
20
+ .option('--since <duration>', 'Replay logs from JetStream (e.g. 5m, 1h, 30s)')
21
+ .option('--level <level>', 'Minimum log level (debug, info, warn, error)', 'debug')
22
+ .option('--json', 'Output raw JSON lines')
23
+ .action(
24
+ async (service: string | undefined, opts: { all?: boolean; since?: string; level: string; json?: boolean }) => {
25
+ if (!service && !opts.all) {
26
+ console.error('Error: specify a service name or use --all');
27
+ process.exit(1);
28
+ }
29
+
30
+ const nc = await connectNats();
31
+ const subject = opts.all ? 'os.o11y.logs.>' : `os.o11y.logs.${service}`;
32
+
33
+ if (opts.since) {
34
+ await replayThenTail(nc, subject, opts);
35
+ } else {
36
+ await liveTail(nc, subject, opts);
37
+ }
38
+ }
39
+ );
40
+
41
+ async function liveTail(
42
+ nc: Awaited<ReturnType<typeof connectNats>>,
43
+ subject: string,
44
+ opts: { level: string; json?: boolean }
45
+ ): Promise<void> {
46
+ const sub = nc.subscribe(subject);
47
+
48
+ for await (const msg of sub) {
49
+ try {
50
+ const entry = JSON.parse(decode(msg.data));
51
+ if (!meetsLevel(entry.level, opts.level)) continue;
52
+
53
+ if (opts.json) {
54
+ process.stdout.write(decode(msg.data) + '\n');
55
+ } else {
56
+ process.stdout.write(formatLogEntry(entry) + '\n');
57
+ }
58
+ } catch {
59
+ // Skip malformed messages
60
+ }
61
+ }
62
+ }
63
+
64
+ async function replayThenTail(
65
+ nc: Awaited<ReturnType<typeof connectNats>>,
66
+ subject: string,
67
+ opts: { since?: string; level: string; json?: boolean }
68
+ ): Promise<void> {
69
+ const sinceMs = parseDuration(opts.since!);
70
+ const startTime = new Date(Date.now() - sinceMs);
71
+
72
+ const js = jetstream(nc);
73
+
74
+ const consumer = await js.consumers.get('OS_O11Y_LOGS', {
75
+ filter_subjects: [subject],
76
+ deliver_policy: DeliverPolicy.StartTime,
77
+ opt_start_time: startTime.toISOString(),
78
+ });
79
+
80
+ const messages = await consumer.consume();
81
+
82
+ for await (const msg of messages) {
83
+ try {
84
+ const entry = JSON.parse(decode(msg.data));
85
+ if (!meetsLevel(entry.level, opts.level)) continue;
86
+
87
+ if (opts.json) {
88
+ process.stdout.write(decode(msg.data) + '\n');
89
+ } else {
90
+ process.stdout.write(formatLogEntry(entry) + '\n');
91
+ }
92
+ } catch {
93
+ // Skip malformed messages
94
+ }
95
+ }
96
+ }
@@ -0,0 +1,53 @@
1
+ import chalk from 'chalk';
2
+ import { Command } from 'commander';
3
+ import { formatHealthTable } from '../lib/formatter.js';
4
+ import { connectNats, decode } from '../lib/nats.js';
5
+
6
+ export const statusCommand = new Command('status')
7
+ .description('Show service health status')
8
+ .option('--watch', 'Continuously watch for health updates')
9
+ .action(async (opts: { watch?: boolean }) => {
10
+ const nc = await connectNats();
11
+ const subject = 'khal._internal.system.health';
12
+
13
+ if (opts.watch) {
14
+ await watchHealth(nc, subject);
15
+ } else {
16
+ await snapshotHealth(nc, subject);
17
+ }
18
+ });
19
+
20
+ async function snapshotHealth(nc: Awaited<ReturnType<typeof connectNats>>, subject: string): Promise<void> {
21
+ const sub = nc.subscribe(subject, { max: 1, timeout: 5000 });
22
+
23
+ process.stdout.write(chalk.dim('Waiting for health data...\n'));
24
+
25
+ try {
26
+ for await (const msg of sub) {
27
+ const data = JSON.parse(decode(msg.data));
28
+ process.stdout.write('\n');
29
+ process.stdout.write(chalk.bold('Service Health') + chalk.dim(` (${data.ts})\n\n`));
30
+ process.stdout.write(formatHealthTable(data.services) + '\n');
31
+ }
32
+ } catch {
33
+ process.stdout.write(chalk.yellow('No health data received within 5s. Is the service-loader running?\n'));
34
+ }
35
+
36
+ await nc.close();
37
+ }
38
+
39
+ async function watchHealth(nc: Awaited<ReturnType<typeof connectNats>>, subject: string): Promise<void> {
40
+ const sub = nc.subscribe(subject);
41
+
42
+ for await (const msg of sub) {
43
+ try {
44
+ const data = JSON.parse(decode(msg.data));
45
+ // Clear screen and redraw
46
+ process.stdout.write('\x1b[2J\x1b[H');
47
+ process.stdout.write(chalk.bold('Service Health') + chalk.dim(` (${data.ts})\n\n`));
48
+ process.stdout.write(formatHealthTable(data.services) + '\n');
49
+ } catch {
50
+ // Skip malformed messages
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,115 @@
1
+ import { DeliverPolicy, jetstream } from '@nats-io/jetstream';
2
+ import chalk from 'chalk';
3
+ import { Command } from 'commander';
4
+ import { connectNats, decode } from '../lib/nats.js';
5
+ import { buildSpanTree, renderSpanTree, type SpanEntry } from '../lib/trace-tree.js';
6
+
7
+ export const tracesCommand = new Command('traces')
8
+ .description('Show distributed trace span tree')
9
+ .argument('<trace-id>', 'Trace ID to look up')
10
+ .option('--json', 'Output raw JSON entries')
11
+ .option('--since <duration>', 'How far back to search in JetStream (default: 1h)', '1h')
12
+ .action(async (traceId: string, opts: { json?: boolean; since: string }) => {
13
+ const nc = await connectNats();
14
+ const entries = await collectTraceEntries(nc, traceId, opts.since);
15
+
16
+ if (entries.length === 0) {
17
+ process.stdout.write(chalk.yellow(`No entries found for trace ${traceId}\n`));
18
+ process.stdout.write(chalk.dim('Hint: events may have expired from the JetStream ring buffer\n'));
19
+ await nc.close();
20
+ return;
21
+ }
22
+
23
+ if (opts.json) {
24
+ for (const entry of entries) {
25
+ process.stdout.write(JSON.stringify(entry) + '\n');
26
+ }
27
+ } else {
28
+ process.stdout.write(chalk.bold('Trace ') + chalk.cyan(traceId) + chalk.dim(` (${entries.length} spans)\n\n`));
29
+ const tree = buildSpanTree(entries);
30
+ process.stdout.write(renderSpanTree(tree) + '\n');
31
+ }
32
+
33
+ await nc.close();
34
+ });
35
+
36
+ /** Parse a duration string like "5m", "1h", "30s" into milliseconds. */
37
+ function parseDuration(input: string): number {
38
+ const match = input.match(/^(\d+)(s|m|h|d)$/);
39
+ if (!match) throw new Error(`Invalid duration: "${input}". Use format like 5m, 1h, 30s`);
40
+ const value = Number.parseInt(match[1], 10);
41
+ const unit = match[2];
42
+ const multipliers: Record<string, number> = { s: 1000, m: 60_000, h: 3_600_000, d: 86_400_000 };
43
+ return value * multipliers[unit];
44
+ }
45
+
46
+ /**
47
+ * Collect all events and logs matching a trace_id from JetStream.
48
+ * Queries both OS_O11Y_EVENTS and OS_O11Y_LOGS streams.
49
+ */
50
+ async function collectTraceEntries(
51
+ nc: Awaited<ReturnType<typeof connectNats>>,
52
+ traceId: string,
53
+ sinceDuration: string
54
+ ): Promise<SpanEntry[]> {
55
+ const sinceMs = parseDuration(sinceDuration);
56
+ const startTime = new Date(Date.now() - sinceMs);
57
+ const js = jetstream(nc);
58
+ const entries: SpanEntry[] = [];
59
+
60
+ // Query events stream
61
+ await drainStream(js, 'OS_O11Y_EVENTS', 'os.o11y.events.>', startTime, traceId, entries, 'event');
62
+
63
+ // Query logs stream
64
+ await drainStream(js, 'OS_O11Y_LOGS', 'os.o11y.logs.>', startTime, traceId, entries, 'log');
65
+
66
+ return entries;
67
+ }
68
+
69
+ async function drainStream(
70
+ js: ReturnType<typeof jetstream>,
71
+ stream: string,
72
+ subject: string,
73
+ startTime: Date,
74
+ traceId: string,
75
+ entries: SpanEntry[],
76
+ type: 'event' | 'log'
77
+ ): Promise<void> {
78
+ try {
79
+ const consumer = await js.consumers.get(stream, {
80
+ filter_subjects: [subject],
81
+ deliver_policy: DeliverPolicy.StartTime,
82
+ opt_start_time: startTime.toISOString(),
83
+ });
84
+
85
+ const messages = await consumer.consume({ max_messages: 5000, expires: 3000 });
86
+
87
+ for await (const msg of messages) {
88
+ try {
89
+ const raw = decode(msg.data);
90
+ const entry = JSON.parse(raw);
91
+
92
+ if (entry.trace_id === traceId) {
93
+ entries.push({
94
+ subject: entry.subject || msg.subject,
95
+ service: entry.service || 'unknown',
96
+ duration_ms: entry.duration_ms ?? 0,
97
+ payload_bytes: entry.payload_bytes,
98
+ trace_id: entry.trace_id,
99
+ span_id: entry.span_id || msg.subject + '-' + entry.ts,
100
+ parent_span_id: entry.parent_span_id,
101
+ ts: entry.ts,
102
+ error: entry.error,
103
+ level: entry.level,
104
+ msg: entry.msg,
105
+ type,
106
+ });
107
+ }
108
+ } catch {
109
+ // Skip malformed messages
110
+ }
111
+ }
112
+ } catch {
113
+ // Stream may not exist or be empty — that's OK
114
+ }
115
+ }
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { eventsCommand } from './commands/events.js';
4
+ import { logsCommand } from './commands/logs.js';
5
+ import { statusCommand } from './commands/status.js';
6
+ import { tracesCommand } from './commands/traces.js';
7
+
8
+ const program = new Command().name('genie-os').description('Genie OS observability CLI').version('0.0.1');
9
+
10
+ program.addCommand(eventsCommand);
11
+ program.addCommand(logsCommand);
12
+ program.addCommand(statusCommand);
13
+ program.addCommand(tracesCommand);
14
+
15
+ program.parse();
@@ -0,0 +1,123 @@
1
+ import chalk from 'chalk';
2
+
3
+ /** Log level icons for human-readable output. */
4
+ const LEVEL_ICONS: Record<string, string> = {
5
+ debug: chalk.gray('DBG'),
6
+ info: chalk.blue('INF'),
7
+ warn: chalk.yellow('WRN'),
8
+ error: chalk.red('ERR'),
9
+ };
10
+
11
+ /** Palette of distinct colors for service names. */
12
+ const SERVICE_COLORS = [
13
+ chalk.cyan,
14
+ chalk.magenta,
15
+ chalk.green,
16
+ chalk.yellow,
17
+ chalk.blue,
18
+ chalk.red,
19
+ chalk.white,
20
+ chalk.gray,
21
+ ];
22
+
23
+ /** Consistent color assignment per service name. */
24
+ const serviceColorMap = new Map<string, (text: string) => string>();
25
+ let colorIndex = 0;
26
+
27
+ function getServiceColor(service: string): (text: string) => string {
28
+ const existing = serviceColorMap.get(service);
29
+ if (existing) return existing;
30
+ const color = SERVICE_COLORS[colorIndex % SERVICE_COLORS.length];
31
+ serviceColorMap.set(service, color);
32
+ colorIndex++;
33
+ return color;
34
+ }
35
+
36
+ interface LogEntry {
37
+ ts: string;
38
+ level: string;
39
+ service: string;
40
+ msg: string;
41
+ error?: { message: string; stack?: string };
42
+ trace_id?: string;
43
+ span_id?: string;
44
+ req_id?: string;
45
+ meta?: Record<string, unknown>;
46
+ }
47
+
48
+ /**
49
+ * Format a log entry as a colored human-readable line.
50
+ * Format: HH:MM:SS.mmm LVL [service] message {context}
51
+ */
52
+ export function formatLogEntry(entry: LogEntry): string {
53
+ const ts = formatTimestamp(entry.ts);
54
+ const level = LEVEL_ICONS[entry.level] || chalk.gray(entry.level.toUpperCase().slice(0, 3));
55
+ const serviceColor = getServiceColor(entry.service);
56
+ const service = serviceColor(`[${entry.service}]`);
57
+ const msg = entry.level === 'error' ? chalk.red(entry.msg) : entry.msg;
58
+
59
+ let line = `${chalk.dim(ts)} ${level} ${service} ${msg}`;
60
+
61
+ if (entry.error) {
62
+ line += ` ${chalk.red(entry.error.message)}`;
63
+ if (entry.error.stack) {
64
+ line += `\n${chalk.dim(entry.error.stack)}`;
65
+ }
66
+ }
67
+
68
+ if (entry.meta && Object.keys(entry.meta).length > 0) {
69
+ line += ` ${chalk.dim(JSON.stringify(entry.meta))}`;
70
+ }
71
+
72
+ if (entry.trace_id) {
73
+ line += chalk.dim(` trace=${entry.trace_id.slice(0, 8)}`);
74
+ }
75
+
76
+ return line;
77
+ }
78
+
79
+ function formatTimestamp(ts: string): string {
80
+ try {
81
+ const d = new Date(ts);
82
+ const h = String(d.getHours()).padStart(2, '0');
83
+ const m = String(d.getMinutes()).padStart(2, '0');
84
+ const s = String(d.getSeconds()).padStart(2, '0');
85
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
86
+ return `${h}:${m}:${s}.${ms}`;
87
+ } catch {
88
+ return ts;
89
+ }
90
+ }
91
+
92
+ /** Ordered log level values for filtering. */
93
+ const LEVEL_VALUES: Record<string, number> = {
94
+ debug: 0,
95
+ info: 1,
96
+ warn: 2,
97
+ error: 3,
98
+ };
99
+
100
+ /** Check if a log level meets the minimum threshold. */
101
+ export function meetsLevel(entryLevel: string, minLevel: string): boolean {
102
+ const entryVal = LEVEL_VALUES[entryLevel] ?? 0;
103
+ const minVal = LEVEL_VALUES[minLevel] ?? 0;
104
+ return entryVal >= minVal;
105
+ }
106
+
107
+ /**
108
+ * Format a health status table row.
109
+ */
110
+ export function formatHealthTable(
111
+ services: Array<{ name: string; running: boolean; retries: number; pid: number | null }>
112
+ ): string {
113
+ const header = `${chalk.bold('Service'.padEnd(25))} ${chalk.bold('Status'.padEnd(10))} ${chalk.bold('PID'.padEnd(10))} ${chalk.bold('Retries')}`;
114
+ const separator = chalk.dim('-'.repeat(60));
115
+ const rows = services.map((s) => {
116
+ const status = s.running ? chalk.green('running') : chalk.red('stopped');
117
+ const pid = s.pid ? String(s.pid) : chalk.dim('-');
118
+ const retries = s.retries > 0 ? chalk.yellow(String(s.retries)) : chalk.dim('0');
119
+ return `${s.name.padEnd(25)} ${status.padEnd(19)} ${pid.padEnd(10)} ${retries}`;
120
+ });
121
+
122
+ return [header, separator, ...rows].join('\n');
123
+ }
@@ -0,0 +1,16 @@
1
+ import { connect, type NatsConnection } from '@nats-io/transport-node';
2
+
3
+ const DEFAULT_NATS_URL = 'nats://localhost:4222';
4
+
5
+ /**
6
+ * Connect to the NATS server. Uses NATS_URL env var or defaults to localhost.
7
+ */
8
+ export async function connectNats(): Promise<NatsConnection> {
9
+ const servers = process.env.NATS_URL || DEFAULT_NATS_URL;
10
+ return connect({ servers });
11
+ }
12
+
13
+ /** Decode NATS message data to a UTF-8 string. */
14
+ export function decode(data: Uint8Array): string {
15
+ return new TextDecoder().decode(data);
16
+ }
@@ -0,0 +1,144 @@
1
+ import chalk from 'chalk';
2
+
3
+ export interface SpanEntry {
4
+ subject: string;
5
+ service: string;
6
+ duration_ms: number;
7
+ payload_bytes?: number;
8
+ trace_id: string;
9
+ span_id: string;
10
+ parent_span_id?: string;
11
+ ts: string;
12
+ error?: { message: string; stack?: string };
13
+ level?: string;
14
+ msg?: string;
15
+ type: 'event' | 'log';
16
+ }
17
+
18
+ interface SpanNode {
19
+ entry: SpanEntry;
20
+ children: SpanNode[];
21
+ }
22
+
23
+ /**
24
+ * Build a tree of spans from a flat list of entries.
25
+ * Entries without a parent_span_id become roots.
26
+ * Orphan entries (parent not found) also become roots.
27
+ */
28
+ export function buildSpanTree(entries: SpanEntry[]): SpanNode[] {
29
+ // Sort by timestamp
30
+ entries.sort((a, b) => new Date(a.ts).getTime() - new Date(b.ts).getTime());
31
+
32
+ const nodeMap = new Map<string, SpanNode>();
33
+ const roots: SpanNode[] = [];
34
+
35
+ // Create nodes
36
+ for (const entry of entries) {
37
+ const node: SpanNode = { entry, children: [] };
38
+ nodeMap.set(entry.span_id, node);
39
+ }
40
+
41
+ // Build tree
42
+ for (const entry of entries) {
43
+ const node = nodeMap.get(entry.span_id)!;
44
+ if (entry.parent_span_id) {
45
+ const parent = nodeMap.get(entry.parent_span_id);
46
+ if (parent) {
47
+ parent.children.push(node);
48
+ } else {
49
+ roots.push(node);
50
+ }
51
+ } else {
52
+ roots.push(node);
53
+ }
54
+ }
55
+
56
+ return roots;
57
+ }
58
+
59
+ /** Get the tree connector character based on position. */
60
+ function connector(isRoot: boolean, isLast: boolean): string {
61
+ if (isRoot) return '';
62
+ return isLast ? '└─ ' : '├─ ';
63
+ }
64
+
65
+ /** Get the continuation prefix for child nodes. */
66
+ function childPrefix(prefix: string, isRoot: boolean, isLast: boolean): string {
67
+ if (isRoot) return prefix;
68
+ return prefix + (isLast ? ' ' : '│ ');
69
+ }
70
+
71
+ /** Format a single span entry as a display line. */
72
+ function formatSpanLine(entry: SpanEntry, linePrefix: string): string {
73
+ const ts = formatTime(entry.ts);
74
+ const service = chalk.cyan(`[${entry.service}]`);
75
+
76
+ if (entry.type === 'log' && entry.msg) {
77
+ const levelTag = entry.level ? chalk.dim(`[${entry.level}]`) : '';
78
+ return `${linePrefix}${ts} ${service} ${levelTag} ${entry.msg}`;
79
+ }
80
+
81
+ const subject = entry.error ? chalk.red(entry.subject) : chalk.white(entry.subject);
82
+ const duration = formatDuration(entry.duration_ms);
83
+ const spanId = chalk.dim(`span=${entry.span_id.slice(0, 8)}`);
84
+ let line = `${linePrefix}${ts} ${service} ${subject} ${duration} ${spanId}`;
85
+
86
+ if (entry.error) {
87
+ line += ` ${chalk.red('ERROR: ' + entry.error.message)}`;
88
+ }
89
+
90
+ return line;
91
+ }
92
+
93
+ /** Append error stack trace lines to the output. */
94
+ function appendErrorStack(lines: string[], entry: SpanEntry, stackPfx: string): void {
95
+ if (!entry.error?.stack) return;
96
+ for (const stackLine of entry.error.stack.split('\n').slice(1, 6)) {
97
+ lines.push(`${stackPfx} ${chalk.dim(stackLine.trim())}`);
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Render a span tree as an indented, colored string for terminal output.
103
+ */
104
+ export function renderSpanTree(roots: SpanNode[]): string {
105
+ const lines: string[] = [];
106
+
107
+ function render(node: SpanNode, prefix: string, isLast: boolean, isRoot: boolean): void {
108
+ const linePrefix = prefix + connector(isRoot, isLast);
109
+ lines.push(formatSpanLine(node.entry, linePrefix));
110
+
111
+ const nextPrefix = childPrefix(prefix, isRoot, isLast);
112
+ appendErrorStack(lines, node.entry, nextPrefix);
113
+
114
+ for (let i = 0; i < node.children.length; i++) {
115
+ render(node.children[i], nextPrefix, i === node.children.length - 1, false);
116
+ }
117
+ }
118
+
119
+ for (let i = 0; i < roots.length; i++) {
120
+ render(roots[i], '', i === roots.length - 1, i === 0 && roots.length === 1);
121
+ }
122
+
123
+ return lines.join('\n');
124
+ }
125
+
126
+ function formatTime(ts: string): string {
127
+ try {
128
+ const d = new Date(ts);
129
+ const h = String(d.getHours()).padStart(2, '0');
130
+ const m = String(d.getMinutes()).padStart(2, '0');
131
+ const s = String(d.getSeconds()).padStart(2, '0');
132
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
133
+ return chalk.dim(`${h}:${m}:${s}.${ms}`);
134
+ } catch {
135
+ return chalk.dim(ts);
136
+ }
137
+ }
138
+
139
+ function formatDuration(ms: number): string {
140
+ if (ms < 1) return chalk.green(`${ms.toFixed(2)}ms`);
141
+ if (ms < 100) return chalk.green(`${ms.toFixed(1)}ms`);
142
+ if (ms < 1000) return chalk.yellow(`${ms.toFixed(0)}ms`);
143
+ return chalk.red(`${(ms / 1000).toFixed(2)}s`);
144
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "esnext",
5
+ "moduleResolution": "bundler",
6
+ "strict": true,
7
+ "noEmit": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true
10
+ },
11
+ "include": ["src/**/*.ts"]
12
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@genie-os/sdk",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "main": "./src/index.ts",
6
+ "exports": {
7
+ ".": "./src/index.ts",
8
+ "./config": "./src/config.ts",
9
+ "./service": "./src/service/runtime.ts",
10
+ "./service/logger": "./src/service/logger.ts",
11
+ "./service/console-intercept": "./src/service/console-intercept.ts",
12
+ "./service/o11y-streams": "./src/service/o11y-streams.ts",
13
+ "./service/trace": "./src/service/trace.ts",
14
+ "./db": "./src/db/factory.ts",
15
+ "./db/migrate": "./src/db/migrate.ts",
16
+ "./db/provision": "./src/db/provision.ts"
17
+ },
18
+ "peerDependencies": {
19
+ "@nats-io/jetstream": ">=3",
20
+ "@nats-io/nats-core": ">=3",
21
+ "@nats-io/transport-node": ">=3",
22
+ "@workos-inc/authkit-nextjs": ">=2",
23
+ "drizzle-orm": ">=0.45",
24
+ "next": ">=16",
25
+ "postgres": ">=3"
26
+ }
27
+ }
@@ -0,0 +1,67 @@
1
+ import { withAuth } from '@workos-inc/authkit-nextjs';
2
+ import { type NextRequest, NextResponse } from 'next/server';
3
+ import { type Database, getDb } from '../db/factory';
4
+
5
+ export interface ApiContext {
6
+ /** Authenticated WorkOS user. Always present (401 returned if not). */
7
+ user: NonNullable<Awaited<ReturnType<typeof withAuth>>['user']>;
8
+ /** Original Next.js request. */
9
+ req: NextRequest;
10
+ }
11
+
12
+ export interface ApiContextWithDb extends ApiContext {
13
+ /** Drizzle database instance. */
14
+ db: Database;
15
+ }
16
+
17
+ type RouteHandler = (req: NextRequest, ...args: unknown[]) => Promise<Response>;
18
+
19
+ /** Synthetic machine user for headless Chrome bypass. */
20
+ const MACHINE_USER = {
21
+ id: 'machine',
22
+ email: 'machine@localhost',
23
+ emailVerified: true,
24
+ firstName: 'Machine',
25
+ lastName: 'User',
26
+ createdAt: new Date().toISOString(),
27
+ updatedAt: new Date().toISOString(),
28
+ object: 'user' as const,
29
+ } as NonNullable<Awaited<ReturnType<typeof withAuth>>['user']>;
30
+
31
+ /** Check if the request is from d3k's headless Chrome with OS_SECRET enabled. */
32
+ function isHeadlessChrome(req: NextRequest): boolean {
33
+ return Boolean(process.env.OS_SECRET && (req.headers.get('user-agent') ?? '').includes('HeadlessChrome'));
34
+ }
35
+
36
+ /**
37
+ * Wraps an API route handler with auth.
38
+ * Returns 401 if the user is not authenticated.
39
+ */
40
+ export function apiHandler(fn: (ctx: ApiContext) => Promise<Response>): RouteHandler {
41
+ return async (req: NextRequest, ..._args: unknown[]) => {
42
+ const auth = await withAuth();
43
+ if (!auth.user && !isHeadlessChrome(req)) {
44
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
45
+ }
46
+
47
+ const user = auth.user ?? MACHINE_USER;
48
+ return fn({ user, req });
49
+ };
50
+ }
51
+
52
+ /**
53
+ * Wraps an API route handler with auth + db resolution.
54
+ * Returns 401 if the user is not authenticated.
55
+ */
56
+ export function apiHandlerWithDb(fn: (ctx: ApiContextWithDb) => Promise<Response>): RouteHandler {
57
+ return async (req: NextRequest, ..._args: unknown[]) => {
58
+ const auth = await withAuth();
59
+ if (!auth.user && !isHeadlessChrome(req)) {
60
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
61
+ }
62
+
63
+ const user = auth.user ?? MACHINE_USER;
64
+ const db = getDb();
65
+ return fn({ user, db, req });
66
+ };
67
+ }