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,165 @@
1
+ 'use client';
2
+
3
+ import { Inbox } from 'lucide-react';
4
+ import { useCallback, useMemo, useState } from 'react';
5
+ import { Button } from '@/components/ui/button';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Spinner } from '@/components/ui/spinner';
8
+ import { useKhalAuth } from '@/lib/auth/use-auth';
9
+ import { useNats } from '@/lib/hooks/use-nats';
10
+ import { SUBJECTS } from '@/lib/subjects';
11
+ import type { LogEntry } from './types';
12
+
13
+ function buildQuickPickSubjects(orgId: string): string[] {
14
+ return [SUBJECTS.echo(orgId), SUBJECTS.pty.create(orgId), SUBJECTS.pty.list(orgId)];
15
+ }
16
+
17
+ type RequestState =
18
+ | { status: 'idle' }
19
+ | { status: 'loading' }
20
+ | { status: 'success'; data: unknown }
21
+ | { status: 'error'; message: string };
22
+
23
+ interface RequestPanelProps {
24
+ /** Optional callback to push an entry to the message buffer. */
25
+ onMessage?: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
26
+ }
27
+
28
+ export function RequestPanel({ onMessage }: RequestPanelProps) {
29
+ const { connected, request } = useNats();
30
+ const auth = useKhalAuth();
31
+ const orgId = auth?.orgId ?? 'default';
32
+ const quickPicks = useMemo(() => buildQuickPickSubjects(orgId), [orgId]);
33
+
34
+ const [subject, setSubject] = useState('');
35
+ const [payload, setPayload] = useState('');
36
+ const [timeout, setTimeout_] = useState(5000);
37
+ const [jsonError, setJsonError] = useState<string | null>(null);
38
+ const [reqState, setReqState] = useState<RequestState>({ status: 'idle' });
39
+
40
+ const handleSend = useCallback(async () => {
41
+ setJsonError(null);
42
+
43
+ let parsed: unknown;
44
+ if (payload.trim()) {
45
+ try {
46
+ parsed = JSON.parse(payload);
47
+ } catch (e) {
48
+ setJsonError(`Invalid JSON: ${(e as Error).message}`);
49
+ return;
50
+ }
51
+ }
52
+
53
+ // Clear previous response and start loading
54
+ setReqState({ status: 'loading' });
55
+
56
+ // Push outgoing request to buffer
57
+ onMessage?.({ subject, payload: parsed, direction: 'out' });
58
+
59
+ try {
60
+ const response = await request(subject, parsed, timeout);
61
+ setReqState({ status: 'success', data: response });
62
+
63
+ // Push response to buffer
64
+ onMessage?.({ subject: `${subject} (reply)`, payload: response, direction: 'in' });
65
+ } catch (e) {
66
+ const message = e instanceof Error ? e.message : String(e);
67
+ setReqState({ status: 'error', message });
68
+ }
69
+ }, [subject, payload, timeout, request, onMessage]);
70
+
71
+ const canSend = subject.trim().length > 0 && connected && reqState.status !== 'loading';
72
+
73
+ return (
74
+ <div className="flex flex-col gap-2">
75
+ {/* Subject input */}
76
+ <Input
77
+ size="small"
78
+ placeholder={SUBJECTS.echo(orgId)}
79
+ value={subject}
80
+ onChange={(e) => {
81
+ setSubject(e.target.value);
82
+ setJsonError(null);
83
+ }}
84
+ />
85
+
86
+ {/* Quick-pick subjects */}
87
+ <div className="flex flex-wrap gap-1">
88
+ {quickPicks.map((s) => (
89
+ <button
90
+ key={s}
91
+ type="button"
92
+ className="rounded border border-gray-alpha-300 px-1.5 py-0.5 text-[10px] font-mono text-gray-800 hover:bg-gray-alpha-100 transition-colors"
93
+ onClick={() => setSubject(s)}
94
+ >
95
+ {s}
96
+ </button>
97
+ ))}
98
+ </div>
99
+
100
+ {/* Payload textarea */}
101
+ <textarea
102
+ className="w-full rounded-md border border-gray-alpha-400 bg-background-100 px-2 py-1.5 font-mono text-xs text-gray-1000 placeholder:text-gray-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-blue-700 focus-visible:ring-offset-1 resize-none"
103
+ rows={4}
104
+ placeholder="{}"
105
+ value={payload}
106
+ onChange={(e) => {
107
+ setPayload(e.target.value);
108
+ setJsonError(null);
109
+ }}
110
+ />
111
+
112
+ {/* Timeout input */}
113
+ <div className="flex items-center gap-2">
114
+ <label className="text-[11px] text-gray-800 shrink-0">Timeout</label>
115
+ <Input
116
+ size="small"
117
+ type="number"
118
+ className="w-20 text-xs"
119
+ value={timeout}
120
+ onChange={(e) => setTimeout_(Number(e.target.value) || 5000)}
121
+ />
122
+ <span className="text-[11px] text-gray-700">ms</span>
123
+ </div>
124
+
125
+ {/* JSON error */}
126
+ {jsonError && <p className="text-[11px] text-red-600">{jsonError}</p>}
127
+
128
+ {/* Send button */}
129
+ <div className="flex items-center gap-2">
130
+ <Button
131
+ size="small"
132
+ disabled={!canSend}
133
+ onClick={handleSend}
134
+ loading={reqState.status === 'loading'}
135
+ prefix={<Inbox className="h-3 w-3" />}
136
+ >
137
+ Send Request
138
+ </Button>
139
+ {!connected && <span className="text-[11px] text-gray-600">Not connected</span>}
140
+ </div>
141
+
142
+ {/* Response area */}
143
+ {reqState.status === 'loading' && (
144
+ <div className="flex items-center gap-2 rounded border border-gray-alpha-300 bg-gray-alpha-50 px-2 py-2">
145
+ <Spinner size="sm" />
146
+ <span className="text-[11px] text-gray-800">Waiting for reply...</span>
147
+ </div>
148
+ )}
149
+
150
+ {reqState.status === 'success' && (
151
+ <div className="rounded border border-gray-alpha-400 bg-gray-alpha-50 p-2 overflow-auto max-h-48">
152
+ <pre className="font-mono text-xs text-gray-1000 whitespace-pre-wrap break-all">
153
+ {typeof reqState.data === 'string' ? reqState.data : JSON.stringify(reqState.data, null, 2)}
154
+ </pre>
155
+ </div>
156
+ )}
157
+
158
+ {reqState.status === 'error' && (
159
+ <div className="rounded border border-red-300 bg-red-50 dark:bg-red-950/20 p-2">
160
+ <p className="font-mono text-[11px] text-red-600 break-all">{reqState.message}</p>
161
+ </div>
162
+ )}
163
+ </div>
164
+ );
165
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { Radio } from 'lucide-react';
4
+ import { Separator } from '@/components/ui/separator';
5
+ import { ActiveSubs } from './ActiveSubs';
6
+ import { useNatsViewer } from './nats-viewer-context';
7
+ import { SubjectCatalog } from './SubjectCatalog';
8
+ import { SubscribeInput } from './SubscribeInput';
9
+
10
+ export function Sidebar() {
11
+ const { subscriptions, addSubscription, removeSubscription } = useNatsViewer();
12
+ const catchAll = 'os.>';
13
+ const catchAllActive = subscriptions.has(catchAll);
14
+
15
+ const toggleCatchAll = () => {
16
+ if (catchAllActive) {
17
+ removeSubscription(catchAll);
18
+ } else {
19
+ addSubscription(catchAll);
20
+ }
21
+ };
22
+
23
+ return (
24
+ <div className="flex flex-col gap-3 overflow-y-auto">
25
+ {/* Catch-all toggle */}
26
+ <button
27
+ onClick={toggleCatchAll}
28
+ className={`flex items-center gap-2 rounded-md px-2 py-1.5 text-xs font-medium transition-colors ${
29
+ catchAllActive
30
+ ? 'bg-green-500/15 text-green-700 hover:bg-green-500/25'
31
+ : 'bg-gray-alpha-100 text-gray-900 hover:bg-gray-alpha-200'
32
+ }`}
33
+ >
34
+ <Radio className="h-3.5 w-3.5" />
35
+ <span className="font-mono">{catchAll}</span>
36
+ <span className="ml-auto text-[11px]">{catchAllActive ? 'ON' : 'OFF'}</span>
37
+ </button>
38
+
39
+ {/* Custom subscribe input */}
40
+ <SubscribeInput />
41
+
42
+ <Separator />
43
+
44
+ {/* Known Subjects */}
45
+ <div>
46
+ <h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-700">Known Subjects</h3>
47
+ <SubjectCatalog />
48
+ </div>
49
+
50
+ <Separator />
51
+
52
+ {/* Active Subscriptions */}
53
+ <div>
54
+ <h3 className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-gray-700">Active Subscriptions</h3>
55
+ <ActiveSubs />
56
+ </div>
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ import { useMemo } from 'react';
4
+ import { useKhalAuth } from '@/lib/auth/use-auth';
5
+ import { useNats } from '@/lib/hooks/use-nats';
6
+ import { SUBJECTS } from '@/lib/subjects';
7
+ import { useNatsViewer } from './nats-viewer-context';
8
+
9
+ /**
10
+ * Build a list of known static subjects.
11
+ * Skips session-scoped subjects like pty.data.
12
+ */
13
+ function buildKnownSubjects(orgId: string, userId: string): string[] {
14
+ const subjects = [
15
+ SUBJECTS.echo(orgId),
16
+ SUBJECTS.system.health(orgId),
17
+ SUBJECTS.pty.create(orgId),
18
+ SUBJECTS.pty.destroy(orgId),
19
+ SUBJECTS.pty.list(orgId),
20
+ SUBJECTS.fs.list(orgId),
21
+ SUBJECTS.fs.read(orgId),
22
+ SUBJECTS.fs.write(orgId),
23
+ SUBJECTS.fs.search(orgId),
24
+ SUBJECTS.notify.broadcast(orgId),
25
+ ];
26
+ if (userId) {
27
+ subjects.push(SUBJECTS.desktop.cmd.all(orgId, userId), SUBJECTS.desktop.event.all(orgId, userId));
28
+ }
29
+ return subjects.sort();
30
+ }
31
+
32
+ export function SubjectCatalog() {
33
+ const { subscriptions, addSubscription, removeSubscription } = useNatsViewer();
34
+ const { userId } = useNats();
35
+ const auth = useKhalAuth();
36
+ const orgId = auth?.orgId ?? 'default';
37
+
38
+ const knownSubjects = useMemo(() => buildKnownSubjects(orgId, userId), [orgId, userId]);
39
+
40
+ return (
41
+ <div className="flex flex-col gap-0.5">
42
+ {knownSubjects.map((subject) => {
43
+ const active = subscriptions.has(subject);
44
+ return (
45
+ <button
46
+ key={subject}
47
+ onClick={() => (active ? removeSubscription(subject) : addSubscription(subject))}
48
+ className="group flex items-center gap-2 rounded px-1.5 py-0.5 text-left transition-colors hover:bg-gray-alpha-100"
49
+ >
50
+ <span
51
+ className={`inline-block h-2 w-2 shrink-0 rounded-full transition-colors ${
52
+ active ? 'bg-green-500' : 'bg-gray-400 group-hover:bg-gray-500'
53
+ }`}
54
+ />
55
+ <span className="min-w-0 truncate font-mono text-xs text-gray-900 group-hover:text-gray-1000">
56
+ {subject}
57
+ </span>
58
+ </button>
59
+ );
60
+ })}
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,59 @@
1
+ 'use client';
2
+
3
+ import { Plus } from 'lucide-react';
4
+ import { useState } from 'react';
5
+ import { useNatsViewer } from './nats-viewer-context';
6
+
7
+ export function SubscribeInput() {
8
+ const { addSubscription } = useNatsViewer();
9
+ const [value, setValue] = useState('');
10
+ const [error, setError] = useState('');
11
+
12
+ const handleSubmit = () => {
13
+ const trimmed = value.trim();
14
+ if (!trimmed) return;
15
+
16
+ if (!trimmed.startsWith('os.')) {
17
+ setError('Subject must start with "os."');
18
+ return;
19
+ }
20
+
21
+ setError('');
22
+ addSubscription(trimmed);
23
+ setValue('');
24
+ };
25
+
26
+ const handleKeyDown = (e: React.KeyboardEvent) => {
27
+ if (e.key === 'Enter') {
28
+ e.preventDefault();
29
+ handleSubmit();
30
+ }
31
+ };
32
+
33
+ return (
34
+ <div className="flex flex-col gap-1">
35
+ <div className="flex items-center gap-1">
36
+ <input
37
+ type="text"
38
+ value={value}
39
+ onChange={(e) => {
40
+ setValue(e.target.value);
41
+ if (error) setError('');
42
+ }}
43
+ onKeyDown={handleKeyDown}
44
+ placeholder="os.custom.subject"
45
+ className="h-7 flex-1 rounded border border-gray-alpha-400 bg-background-100 px-2 font-mono text-xs text-gray-1000 placeholder:text-gray-600 focus:outline-none focus:ring-1 focus:ring-blue-700"
46
+ />
47
+ <button
48
+ onClick={handleSubmit}
49
+ disabled={!value.trim()}
50
+ className="flex h-7 shrink-0 items-center gap-1 rounded border border-gray-alpha-400 bg-background-100 px-2 text-xs text-gray-900 transition-colors hover:bg-gray-alpha-100 hover:text-gray-1000 disabled:opacity-40 disabled:pointer-events-none"
51
+ >
52
+ <Plus className="h-3 w-3" />
53
+ Sub
54
+ </button>
55
+ </div>
56
+ {error && <p className="px-0.5 text-[11px] text-red-600">{error}</p>}
57
+ </div>
58
+ );
59
+ }
@@ -0,0 +1,5 @@
1
+ export { MessageLog } from './MessageLog';
2
+ export type { NatsViewerContextValue } from './NatsViewer';
3
+ export { NatsViewer, useNatsViewer } from './NatsViewer';
4
+ export type { LogEntry } from './types';
5
+ export { useMessageBuffer } from './use-message-buffer';
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext } from 'react';
4
+ import type { LogEntry } from './types';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Context — shared state for sidebar panels and the main log area
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export interface NatsViewerContextValue {
11
+ subscriptions: Set<string>;
12
+ addSubscription: (subject: string) => void;
13
+ removeSubscription: (subject: string) => void;
14
+ buffer: {
15
+ entries: LogEntry[];
16
+ push: (entry: Omit<LogEntry, 'id' | 'timestamp'>) => void;
17
+ clear: () => void;
18
+ };
19
+ filter: string;
20
+ setFilter: (f: string) => void;
21
+ paused: boolean;
22
+ setPaused: (p: boolean) => void;
23
+ }
24
+
25
+ export const NatsViewerContext = createContext<NatsViewerContextValue | null>(null);
26
+
27
+ export function useNatsViewer(): NatsViewerContextValue {
28
+ const ctx = useContext(NatsViewerContext);
29
+ if (!ctx) throw new Error('useNatsViewer must be used within <NatsViewer>');
30
+ return ctx;
31
+ }
@@ -0,0 +1,7 @@
1
+ export interface LogEntry {
2
+ id: string;
3
+ timestamp: number;
4
+ subject: string;
5
+ payload: unknown;
6
+ direction: 'in' | 'out'; // 'in' = received, 'out' = published by user
7
+ }
@@ -0,0 +1,55 @@
1
+ import { useCallback, useReducer } from 'react';
2
+ import type { LogEntry } from './types';
3
+
4
+ const DEFAULT_CAPACITY = 1000;
5
+
6
+ type Action =
7
+ | { type: 'push'; entry: Omit<LogEntry, 'id' | 'timestamp'> }
8
+ | { type: 'clear' }
9
+ | { type: 'set-capacity'; capacity: number };
10
+
11
+ interface State {
12
+ entries: LogEntry[];
13
+ capacity: number;
14
+ }
15
+
16
+ function reducer(state: State, action: Action): State {
17
+ switch (action.type) {
18
+ case 'push': {
19
+ const entry: LogEntry = {
20
+ ...action.entry,
21
+ id: crypto.randomUUID(),
22
+ timestamp: Date.now(),
23
+ };
24
+ const next = [...state.entries, entry];
25
+ // Drop oldest entries when over capacity
26
+ if (next.length > state.capacity) {
27
+ return { ...state, entries: next.slice(next.length - state.capacity) };
28
+ }
29
+ return { ...state, entries: next };
30
+ }
31
+ case 'clear':
32
+ return { ...state, entries: [] };
33
+ case 'set-capacity': {
34
+ const entries =
35
+ state.entries.length > action.capacity
36
+ ? state.entries.slice(state.entries.length - action.capacity)
37
+ : state.entries;
38
+ return { capacity: action.capacity, entries };
39
+ }
40
+ }
41
+ }
42
+
43
+ export function useMessageBuffer(capacity = DEFAULT_CAPACITY) {
44
+ const [state, dispatch] = useReducer(reducer, { entries: [], capacity });
45
+
46
+ const push = useCallback((entry: Omit<LogEntry, 'id' | 'timestamp'>) => {
47
+ dispatch({ type: 'push', entry });
48
+ }, []);
49
+
50
+ const clear = useCallback(() => {
51
+ dispatch({ type: 'clear' });
52
+ }, []);
53
+
54
+ return { entries: state.entries, push, clear };
55
+ }
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@genie-os/cli",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "bin": {
7
+ "genie-os": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "bun build src/index.ts --outfile dist/index.js --target node"
11
+ },
12
+ "dependencies": {
13
+ "commander": "^12.1.0",
14
+ "chalk": "^5.4.1",
15
+ "@nats-io/transport-node": "^3.3.1",
16
+ "@nats-io/jetstream": "^3.3.1"
17
+ }
18
+ }
@@ -0,0 +1,176 @@
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
+
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
+ interface EventEntry {
17
+ subject: string;
18
+ service: string;
19
+ duration_ms: number;
20
+ payload_bytes?: number;
21
+ trace_id: string;
22
+ span_id: string;
23
+ parent_span_id?: string;
24
+ ts: string;
25
+ error?: { message: string; stack?: string };
26
+ }
27
+
28
+ /** Palette of distinct colors for service names. */
29
+ const SERVICE_COLORS = [
30
+ chalk.cyan,
31
+ chalk.magenta,
32
+ chalk.green,
33
+ chalk.yellow,
34
+ chalk.blue,
35
+ chalk.red,
36
+ chalk.white,
37
+ chalk.gray,
38
+ ];
39
+ const serviceColorMap = new Map<string, (text: string) => string>();
40
+ let colorIndex = 0;
41
+
42
+ function getServiceColor(service: string): (text: string) => string {
43
+ const existing = serviceColorMap.get(service);
44
+ if (existing) return existing;
45
+ const color = SERVICE_COLORS[colorIndex % SERVICE_COLORS.length];
46
+ serviceColorMap.set(service, color);
47
+ colorIndex++;
48
+ return color;
49
+ }
50
+
51
+ function formatEvent(entry: EventEntry): string {
52
+ const ts = formatTimestamp(entry.ts);
53
+ const serviceColor = getServiceColor(entry.service);
54
+ const service = serviceColor(`[${entry.service}]`);
55
+ const subject = chalk.white(entry.subject);
56
+ const hasError = !!entry.error;
57
+
58
+ const durationMs = entry.duration_ms;
59
+ let duration: string;
60
+ if (durationMs < 1) duration = chalk.green(`${durationMs.toFixed(2)}ms`);
61
+ else if (durationMs < 100) duration = chalk.green(`${durationMs.toFixed(1)}ms`);
62
+ else if (durationMs < 1000) duration = chalk.yellow(`${durationMs.toFixed(0)}ms`);
63
+ else duration = chalk.red(`${(durationMs / 1000).toFixed(2)}s`);
64
+
65
+ const status = hasError ? chalk.red('ERR') : chalk.green('OK');
66
+ const traceId = entry.trace_id ? chalk.dim(`trace=${entry.trace_id.slice(0, 8)}`) : '';
67
+
68
+ let line = `${chalk.dim(ts)} ${status} ${service} ${subject} ${duration} ${traceId}`;
69
+
70
+ if (hasError && entry.error) {
71
+ line += `\n ${chalk.red(entry.error.message)}`;
72
+ if (entry.error.stack) {
73
+ const stackLines = entry.error.stack.split('\n').slice(1, 4);
74
+ for (const sl of stackLines) {
75
+ line += `\n ${chalk.dim(sl.trim())}`;
76
+ }
77
+ }
78
+ }
79
+
80
+ return line;
81
+ }
82
+
83
+ function formatTimestamp(ts: string): string {
84
+ try {
85
+ const d = new Date(ts);
86
+ const h = String(d.getHours()).padStart(2, '0');
87
+ const m = String(d.getMinutes()).padStart(2, '0');
88
+ const s = String(d.getSeconds()).padStart(2, '0');
89
+ const ms = String(d.getMilliseconds()).padStart(3, '0');
90
+ return `${h}:${m}:${s}.${ms}`;
91
+ } catch {
92
+ return ts;
93
+ }
94
+ }
95
+
96
+ export const eventsCommand = new Command('events')
97
+ .description('Tail handler events with timing and status')
98
+ .argument('[subject-pattern]', 'NATS subject pattern to filter events (e.g. os.genie.teams.*)')
99
+ .option('--all', 'Subscribe to all handler events')
100
+ .option('--since <duration>', 'Replay events from JetStream (e.g. 5m, 1h, 30s)')
101
+ .option('--json', 'Output raw JSON lines')
102
+ .action(async (subjectPattern: string | undefined, opts: { all?: boolean; since?: string; json?: boolean }) => {
103
+ if (!subjectPattern && !opts.all) {
104
+ console.error('Error: specify a subject pattern or use --all');
105
+ process.exit(1);
106
+ }
107
+
108
+ const nc = await connectNats();
109
+
110
+ // Build the NATS subject to subscribe to.
111
+ // User passes a pattern like "os.genie.teams.*" — we prefix with "os.o11y.events."
112
+ const subject = opts.all ? 'os.o11y.events.>' : `os.o11y.events.${subjectPattern}`;
113
+
114
+ if (opts.since) {
115
+ await replayThenTail(nc, subject, opts);
116
+ } else {
117
+ await liveTail(nc, subject, opts);
118
+ }
119
+ });
120
+
121
+ async function liveTail(
122
+ nc: Awaited<ReturnType<typeof connectNats>>,
123
+ subject: string,
124
+ opts: { json?: boolean }
125
+ ): Promise<void> {
126
+ const sub = nc.subscribe(subject);
127
+
128
+ for await (const msg of sub) {
129
+ try {
130
+ const raw = decode(msg.data);
131
+ const entry: EventEntry = JSON.parse(raw);
132
+
133
+ if (opts.json) {
134
+ process.stdout.write(raw + '\n');
135
+ } else {
136
+ process.stdout.write(formatEvent(entry) + '\n');
137
+ }
138
+ } catch {
139
+ // Skip malformed messages
140
+ }
141
+ }
142
+ }
143
+
144
+ async function replayThenTail(
145
+ nc: Awaited<ReturnType<typeof connectNats>>,
146
+ subject: string,
147
+ opts: { since?: string; json?: boolean }
148
+ ): Promise<void> {
149
+ const sinceMs = parseDuration(opts.since!);
150
+ const startTime = new Date(Date.now() - sinceMs);
151
+
152
+ const js = jetstream(nc);
153
+
154
+ const consumer = await js.consumers.get('OS_O11Y_EVENTS', {
155
+ filter_subjects: [subject],
156
+ deliver_policy: DeliverPolicy.StartTime,
157
+ opt_start_time: startTime.toISOString(),
158
+ });
159
+
160
+ const messages = await consumer.consume();
161
+
162
+ for await (const msg of messages) {
163
+ try {
164
+ const raw = decode(msg.data);
165
+ const entry: EventEntry = JSON.parse(raw);
166
+
167
+ if (opts.json) {
168
+ process.stdout.write(raw + '\n');
169
+ } else {
170
+ process.stdout.write(formatEvent(entry) + '\n');
171
+ }
172
+ } catch {
173
+ // Skip malformed messages
174
+ }
175
+ }
176
+ }