gitspace 0.2.0-rc.1

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 (318) hide show
  1. package/.claude/settings.local.json +21 -0
  2. package/.gitspace/bundle.json +50 -0
  3. package/.gitspace/select/01-status.sh +40 -0
  4. package/.gitspace/setup/01-install-deps.sh +12 -0
  5. package/.gitspace/setup/02-typecheck.sh +16 -0
  6. package/AGENTS.md +439 -0
  7. package/CLAUDE.md +1 -0
  8. package/LICENSE +25 -0
  9. package/README.md +607 -0
  10. package/bin/gssh +62 -0
  11. package/bun.lock +647 -0
  12. package/docs/CONNECTION.md +623 -0
  13. package/docs/GATEWAY-WORKER.md +319 -0
  14. package/docs/GETTING-STARTED.md +448 -0
  15. package/docs/GITSPACE-PLATFORM.md +1819 -0
  16. package/docs/INFRASTRUCTURE.md +1347 -0
  17. package/docs/PROTOCOL.md +619 -0
  18. package/docs/QUICKSTART.md +174 -0
  19. package/docs/RELAY.md +327 -0
  20. package/docs/REMOTE-DESIGN.md +549 -0
  21. package/docs/ROADMAP.md +564 -0
  22. package/docs/SITE_DOCS_FIGMA_MAKE.md +1167 -0
  23. package/docs/STACK-DESIGN.md +588 -0
  24. package/docs/UNIFIED_ARCHITECTURE.md +292 -0
  25. package/experiments/pty-benchmark.ts +148 -0
  26. package/experiments/pty-latency.ts +100 -0
  27. package/experiments/router/client.ts +199 -0
  28. package/experiments/router/protocol.ts +74 -0
  29. package/experiments/router/router.ts +217 -0
  30. package/experiments/router/session.ts +180 -0
  31. package/experiments/router/test.ts +133 -0
  32. package/experiments/socket-bandwidth.ts +77 -0
  33. package/homebrew/gitspace.rb +45 -0
  34. package/landing-page/ATTRIBUTIONS.md +3 -0
  35. package/landing-page/README.md +11 -0
  36. package/landing-page/bun.lock +801 -0
  37. package/landing-page/guidelines/Guidelines.md +61 -0
  38. package/landing-page/index.html +37 -0
  39. package/landing-page/package.json +90 -0
  40. package/landing-page/postcss.config.mjs +15 -0
  41. package/landing-page/public/_redirects +1 -0
  42. package/landing-page/public/favicon.png +0 -0
  43. package/landing-page/src/app/App.tsx +53 -0
  44. package/landing-page/src/app/components/figma/ImageWithFallback.tsx +27 -0
  45. package/landing-page/src/app/components/ui/accordion.tsx +66 -0
  46. package/landing-page/src/app/components/ui/alert-dialog.tsx +157 -0
  47. package/landing-page/src/app/components/ui/alert.tsx +66 -0
  48. package/landing-page/src/app/components/ui/aspect-ratio.tsx +11 -0
  49. package/landing-page/src/app/components/ui/avatar.tsx +53 -0
  50. package/landing-page/src/app/components/ui/badge.tsx +46 -0
  51. package/landing-page/src/app/components/ui/breadcrumb.tsx +109 -0
  52. package/landing-page/src/app/components/ui/button.tsx +57 -0
  53. package/landing-page/src/app/components/ui/calendar.tsx +75 -0
  54. package/landing-page/src/app/components/ui/card.tsx +92 -0
  55. package/landing-page/src/app/components/ui/carousel.tsx +241 -0
  56. package/landing-page/src/app/components/ui/chart.tsx +353 -0
  57. package/landing-page/src/app/components/ui/checkbox.tsx +32 -0
  58. package/landing-page/src/app/components/ui/collapsible.tsx +33 -0
  59. package/landing-page/src/app/components/ui/command.tsx +177 -0
  60. package/landing-page/src/app/components/ui/context-menu.tsx +252 -0
  61. package/landing-page/src/app/components/ui/dialog.tsx +135 -0
  62. package/landing-page/src/app/components/ui/drawer.tsx +132 -0
  63. package/landing-page/src/app/components/ui/dropdown-menu.tsx +257 -0
  64. package/landing-page/src/app/components/ui/form.tsx +168 -0
  65. package/landing-page/src/app/components/ui/hover-card.tsx +44 -0
  66. package/landing-page/src/app/components/ui/input-otp.tsx +77 -0
  67. package/landing-page/src/app/components/ui/input.tsx +21 -0
  68. package/landing-page/src/app/components/ui/label.tsx +24 -0
  69. package/landing-page/src/app/components/ui/menubar.tsx +276 -0
  70. package/landing-page/src/app/components/ui/navigation-menu.tsx +168 -0
  71. package/landing-page/src/app/components/ui/pagination.tsx +127 -0
  72. package/landing-page/src/app/components/ui/popover.tsx +48 -0
  73. package/landing-page/src/app/components/ui/progress.tsx +31 -0
  74. package/landing-page/src/app/components/ui/radio-group.tsx +45 -0
  75. package/landing-page/src/app/components/ui/resizable.tsx +56 -0
  76. package/landing-page/src/app/components/ui/scroll-area.tsx +58 -0
  77. package/landing-page/src/app/components/ui/select.tsx +189 -0
  78. package/landing-page/src/app/components/ui/separator.tsx +28 -0
  79. package/landing-page/src/app/components/ui/sheet.tsx +139 -0
  80. package/landing-page/src/app/components/ui/sidebar.tsx +726 -0
  81. package/landing-page/src/app/components/ui/skeleton.tsx +13 -0
  82. package/landing-page/src/app/components/ui/slider.tsx +63 -0
  83. package/landing-page/src/app/components/ui/sonner.tsx +25 -0
  84. package/landing-page/src/app/components/ui/switch.tsx +31 -0
  85. package/landing-page/src/app/components/ui/table.tsx +116 -0
  86. package/landing-page/src/app/components/ui/tabs.tsx +66 -0
  87. package/landing-page/src/app/components/ui/textarea.tsx +18 -0
  88. package/landing-page/src/app/components/ui/toggle-group.tsx +73 -0
  89. package/landing-page/src/app/components/ui/toggle.tsx +47 -0
  90. package/landing-page/src/app/components/ui/tooltip.tsx +61 -0
  91. package/landing-page/src/app/components/ui/use-mobile.ts +21 -0
  92. package/landing-page/src/app/components/ui/utils.ts +6 -0
  93. package/landing-page/src/components/docs/DocsContent.tsx +718 -0
  94. package/landing-page/src/components/docs/DocsSidebar.tsx +84 -0
  95. package/landing-page/src/components/landing/CTA.tsx +59 -0
  96. package/landing-page/src/components/landing/Comparison.tsx +84 -0
  97. package/landing-page/src/components/landing/FaultyTerminal.tsx +424 -0
  98. package/landing-page/src/components/landing/Features.tsx +201 -0
  99. package/landing-page/src/components/landing/Hero.tsx +142 -0
  100. package/landing-page/src/components/landing/Pricing.tsx +140 -0
  101. package/landing-page/src/components/landing/Roadmap.tsx +86 -0
  102. package/landing-page/src/components/landing/Security.tsx +81 -0
  103. package/landing-page/src/components/landing/TerminalWindow.tsx +27 -0
  104. package/landing-page/src/components/landing/UseCases.tsx +55 -0
  105. package/landing-page/src/components/landing/Workflow.tsx +101 -0
  106. package/landing-page/src/components/layout/DashboardNavbar.tsx +37 -0
  107. package/landing-page/src/components/layout/Footer.tsx +55 -0
  108. package/landing-page/src/components/layout/LandingNavbar.tsx +82 -0
  109. package/landing-page/src/components/ui/badge.tsx +39 -0
  110. package/landing-page/src/components/ui/breadcrumb.tsx +115 -0
  111. package/landing-page/src/components/ui/button.tsx +57 -0
  112. package/landing-page/src/components/ui/card.tsx +79 -0
  113. package/landing-page/src/components/ui/mock-terminal.tsx +68 -0
  114. package/landing-page/src/components/ui/separator.tsx +28 -0
  115. package/landing-page/src/lib/utils.ts +6 -0
  116. package/landing-page/src/main.tsx +10 -0
  117. package/landing-page/src/pages/Dashboard.tsx +133 -0
  118. package/landing-page/src/pages/DocsPage.tsx +79 -0
  119. package/landing-page/src/pages/LandingPage.tsx +31 -0
  120. package/landing-page/src/pages/TerminalView.tsx +106 -0
  121. package/landing-page/src/styles/fonts.css +0 -0
  122. package/landing-page/src/styles/index.css +3 -0
  123. package/landing-page/src/styles/tailwind.css +4 -0
  124. package/landing-page/src/styles/theme.css +181 -0
  125. package/landing-page/vite.config.ts +19 -0
  126. package/npm/darwin-arm64/bin/gssh +0 -0
  127. package/npm/darwin-arm64/package.json +20 -0
  128. package/package.json +74 -0
  129. package/scripts/build.ts +284 -0
  130. package/scripts/release.ts +140 -0
  131. package/src/__tests__/test-utils.ts +298 -0
  132. package/src/commands/__tests__/serve-messages.test.ts +190 -0
  133. package/src/commands/access.ts +298 -0
  134. package/src/commands/add.ts +452 -0
  135. package/src/commands/auth.ts +364 -0
  136. package/src/commands/connect.ts +287 -0
  137. package/src/commands/directory.ts +16 -0
  138. package/src/commands/host.ts +396 -0
  139. package/src/commands/identity.ts +184 -0
  140. package/src/commands/list.ts +200 -0
  141. package/src/commands/relay.ts +315 -0
  142. package/src/commands/remove.ts +241 -0
  143. package/src/commands/serve.ts +1493 -0
  144. package/src/commands/share.ts +456 -0
  145. package/src/commands/status.ts +125 -0
  146. package/src/commands/switch.ts +353 -0
  147. package/src/commands/tmux.ts +317 -0
  148. package/src/core/__tests__/access.test.ts +240 -0
  149. package/src/core/access.ts +277 -0
  150. package/src/core/bundle.ts +342 -0
  151. package/src/core/config.ts +510 -0
  152. package/src/core/git.ts +317 -0
  153. package/src/core/github.ts +151 -0
  154. package/src/core/identity.ts +631 -0
  155. package/src/core/linear.ts +225 -0
  156. package/src/core/shell.ts +161 -0
  157. package/src/core/trusted-relays.ts +315 -0
  158. package/src/index.ts +821 -0
  159. package/src/lib/remote-session/index.ts +7 -0
  160. package/src/lib/remote-session/protocol.ts +267 -0
  161. package/src/lib/remote-session/session-handler.ts +581 -0
  162. package/src/lib/remote-session/workspace-scanner.ts +167 -0
  163. package/src/lib/tmux-lite/README.md +81 -0
  164. package/src/lib/tmux-lite/cli.ts +796 -0
  165. package/src/lib/tmux-lite/crypto/__tests__/helpers/handshake-runner.ts +349 -0
  166. package/src/lib/tmux-lite/crypto/__tests__/helpers/mock-relay.ts +291 -0
  167. package/src/lib/tmux-lite/crypto/__tests__/helpers/test-identities.ts +142 -0
  168. package/src/lib/tmux-lite/crypto/__tests__/integration/authorization.integration.test.ts +339 -0
  169. package/src/lib/tmux-lite/crypto/__tests__/integration/e2e-communication.integration.test.ts +477 -0
  170. package/src/lib/tmux-lite/crypto/__tests__/integration/error-handling.integration.test.ts +499 -0
  171. package/src/lib/tmux-lite/crypto/__tests__/integration/handshake.integration.test.ts +371 -0
  172. package/src/lib/tmux-lite/crypto/__tests__/integration/security.integration.test.ts +573 -0
  173. package/src/lib/tmux-lite/crypto/access-control.test.ts +512 -0
  174. package/src/lib/tmux-lite/crypto/access-control.ts +320 -0
  175. package/src/lib/tmux-lite/crypto/frames.test.ts +262 -0
  176. package/src/lib/tmux-lite/crypto/frames.ts +141 -0
  177. package/src/lib/tmux-lite/crypto/handshake.ts +894 -0
  178. package/src/lib/tmux-lite/crypto/identity.test.ts +220 -0
  179. package/src/lib/tmux-lite/crypto/identity.ts +286 -0
  180. package/src/lib/tmux-lite/crypto/index.ts +51 -0
  181. package/src/lib/tmux-lite/crypto/invites.test.ts +381 -0
  182. package/src/lib/tmux-lite/crypto/invites.ts +215 -0
  183. package/src/lib/tmux-lite/crypto/keyexchange.ts +435 -0
  184. package/src/lib/tmux-lite/crypto/keys.test.ts +58 -0
  185. package/src/lib/tmux-lite/crypto/keys.ts +47 -0
  186. package/src/lib/tmux-lite/crypto/secretbox.test.ts +169 -0
  187. package/src/lib/tmux-lite/crypto/secretbox.ts +124 -0
  188. package/src/lib/tmux-lite/handshake-handler.ts +451 -0
  189. package/src/lib/tmux-lite/protocol.test.ts +307 -0
  190. package/src/lib/tmux-lite/protocol.ts +266 -0
  191. package/src/lib/tmux-lite/relay-client.ts +506 -0
  192. package/src/lib/tmux-lite/server.ts +1250 -0
  193. package/src/lib/tmux-lite/shell-integration.sh +37 -0
  194. package/src/lib/tmux-lite/terminal-queries.test.ts +54 -0
  195. package/src/lib/tmux-lite/terminal-queries.ts +49 -0
  196. package/src/relay/__tests__/e2e-flow.test.ts +1284 -0
  197. package/src/relay/__tests__/helpers/auth.ts +354 -0
  198. package/src/relay/__tests__/helpers/ports.ts +51 -0
  199. package/src/relay/__tests__/protocol-validation.test.ts +265 -0
  200. package/src/relay/authorization.ts +303 -0
  201. package/src/relay/embedded-assets.generated.d.ts +15 -0
  202. package/src/relay/identity.ts +352 -0
  203. package/src/relay/index.ts +57 -0
  204. package/src/relay/pipes.test.ts +427 -0
  205. package/src/relay/pipes.ts +195 -0
  206. package/src/relay/protocol.ts +804 -0
  207. package/src/relay/registries.test.ts +437 -0
  208. package/src/relay/registries.ts +593 -0
  209. package/src/relay/server.test.ts +1323 -0
  210. package/src/relay/server.ts +1092 -0
  211. package/src/relay/signing.ts +238 -0
  212. package/src/relay/types.ts +69 -0
  213. package/src/serve/client-session-manager.ts +622 -0
  214. package/src/serve/daemon.ts +497 -0
  215. package/src/serve/pty-session.ts +236 -0
  216. package/src/serve/types.ts +169 -0
  217. package/src/shared/components/Flow.tsx +453 -0
  218. package/src/shared/components/Flow.tui.tsx +343 -0
  219. package/src/shared/components/Flow.web.tsx +442 -0
  220. package/src/shared/components/Inbox.tsx +446 -0
  221. package/src/shared/components/Inbox.tui.tsx +262 -0
  222. package/src/shared/components/Inbox.web.tsx +329 -0
  223. package/src/shared/components/MachineList.tsx +187 -0
  224. package/src/shared/components/MachineList.tui.tsx +161 -0
  225. package/src/shared/components/MachineList.web.tsx +210 -0
  226. package/src/shared/components/ProjectList.tsx +176 -0
  227. package/src/shared/components/ProjectList.tui.tsx +109 -0
  228. package/src/shared/components/ProjectList.web.tsx +143 -0
  229. package/src/shared/components/SpacesBrowser.tsx +332 -0
  230. package/src/shared/components/SpacesBrowser.tui.tsx +163 -0
  231. package/src/shared/components/SpacesBrowser.web.tsx +221 -0
  232. package/src/shared/components/index.ts +103 -0
  233. package/src/shared/hooks/index.ts +16 -0
  234. package/src/shared/hooks/useNavigation.ts +226 -0
  235. package/src/shared/index.ts +122 -0
  236. package/src/shared/providers/LocalMachineProvider.ts +425 -0
  237. package/src/shared/providers/MachineProvider.ts +165 -0
  238. package/src/shared/providers/RemoteMachineProvider.ts +444 -0
  239. package/src/shared/providers/index.ts +26 -0
  240. package/src/shared/types.ts +145 -0
  241. package/src/tui/adapters.ts +120 -0
  242. package/src/tui/app.tsx +1816 -0
  243. package/src/tui/components/Terminal.tsx +580 -0
  244. package/src/tui/hooks/index.ts +35 -0
  245. package/src/tui/hooks/useAppState.ts +314 -0
  246. package/src/tui/hooks/useDaemonStatus.ts +174 -0
  247. package/src/tui/hooks/useInboxTUI.ts +113 -0
  248. package/src/tui/hooks/useRemoteMachines.ts +209 -0
  249. package/src/tui/index.ts +24 -0
  250. package/src/tui/state.ts +299 -0
  251. package/src/tui/terminal-bracketed-paste.test.ts +45 -0
  252. package/src/tui/terminal-bracketed-paste.ts +47 -0
  253. package/src/types/bundle.ts +112 -0
  254. package/src/types/config.ts +89 -0
  255. package/src/types/errors.ts +206 -0
  256. package/src/types/identity.ts +284 -0
  257. package/src/types/workspace-fuzzy.ts +49 -0
  258. package/src/types/workspace.ts +151 -0
  259. package/src/utils/bun-socket-writer.ts +80 -0
  260. package/src/utils/deps.ts +127 -0
  261. package/src/utils/fuzzy-match.ts +125 -0
  262. package/src/utils/logger.ts +127 -0
  263. package/src/utils/markdown.ts +254 -0
  264. package/src/utils/onboarding.ts +229 -0
  265. package/src/utils/prompts.ts +114 -0
  266. package/src/utils/run-commands.ts +112 -0
  267. package/src/utils/run-scripts.ts +142 -0
  268. package/src/utils/sanitize.ts +98 -0
  269. package/src/utils/secrets.ts +122 -0
  270. package/src/utils/shell-escape.ts +40 -0
  271. package/src/utils/utf8.ts +79 -0
  272. package/src/utils/workspace-state.ts +47 -0
  273. package/src/web/README.md +73 -0
  274. package/src/web/bun.lock +575 -0
  275. package/src/web/eslint.config.js +23 -0
  276. package/src/web/index.html +16 -0
  277. package/src/web/package.json +37 -0
  278. package/src/web/public/vite.svg +1 -0
  279. package/src/web/src/App.tsx +604 -0
  280. package/src/web/src/assets/react.svg +1 -0
  281. package/src/web/src/components/Terminal.tsx +207 -0
  282. package/src/web/src/hooks/useRelayConnection.ts +224 -0
  283. package/src/web/src/hooks/useTerminal.ts +699 -0
  284. package/src/web/src/index.css +55 -0
  285. package/src/web/src/lib/crypto/__tests__/web-terminal.test.ts +1158 -0
  286. package/src/web/src/lib/crypto/frames.ts +205 -0
  287. package/src/web/src/lib/crypto/handshake.ts +396 -0
  288. package/src/web/src/lib/crypto/identity.ts +128 -0
  289. package/src/web/src/lib/crypto/keyexchange.ts +246 -0
  290. package/src/web/src/lib/crypto/relay-signing.ts +53 -0
  291. package/src/web/src/lib/invite.ts +58 -0
  292. package/src/web/src/lib/storage/identity-store.ts +94 -0
  293. package/src/web/src/main.tsx +10 -0
  294. package/src/web/src/types/identity.ts +45 -0
  295. package/src/web/tsconfig.app.json +28 -0
  296. package/src/web/tsconfig.json +7 -0
  297. package/src/web/tsconfig.node.json +26 -0
  298. package/src/web/vite.config.ts +31 -0
  299. package/todo-security.md +92 -0
  300. package/tsconfig.json +23 -0
  301. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite +0 -0
  302. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-shm +0 -0
  303. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/12b7107e435bf1b9a8713a7f320472a63e543104d633d89a26f8d21f4e4ef182.sqlite-wal +0 -0
  304. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite +0 -0
  305. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-shm +0 -0
  306. package/worker/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1a1ac3db1ab86ecf712f90322868a9aabc2c7dc9fe2dfbe94f9b075096276b0f.sqlite-wal +0 -0
  307. package/worker/bun.lock +237 -0
  308. package/worker/package.json +22 -0
  309. package/worker/schema.sql +96 -0
  310. package/worker/src/handlers/auth.ts +451 -0
  311. package/worker/src/handlers/subdomains.ts +376 -0
  312. package/worker/src/handlers/user.ts +98 -0
  313. package/worker/src/index.ts +70 -0
  314. package/worker/src/middleware/auth.ts +152 -0
  315. package/worker/src/services/cloudflare.ts +609 -0
  316. package/worker/src/types.ts +96 -0
  317. package/worker/tsconfig.json +15 -0
  318. package/worker/wrangler.toml +26 -0
@@ -0,0 +1,796 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * tmux-lite CLI and API
4
+ *
5
+ * CLI Commands:
6
+ * tl new [name] Create new session
7
+ * tl a|attach [id] Attach to session
8
+ * tl ls|list List sessions
9
+ * tl kill <id> Kill a session
10
+ * tl kill-server Stop the server
11
+ *
12
+ * API: Import and use the exported functions
13
+ */
14
+
15
+ import { spawn } from "bun";
16
+ import { existsSync, readFileSync, unlinkSync } from "fs";
17
+ import { select } from "@inquirer/prompts";
18
+ import { createBufferedSocketWriter } from "../../utils/bun-socket-writer";
19
+ import {
20
+ getRouterSocket,
21
+ getPidFile,
22
+ PROTOCOL_VERSION,
23
+ PACKAGE_VERSION,
24
+ type Command,
25
+ type Response,
26
+ type Session,
27
+ type SessionEvent,
28
+ type InboxItem,
29
+ encodeRouterMessage,
30
+ decodeRouterMessages,
31
+ encodeControl,
32
+ encodePTY,
33
+ parseFrames,
34
+ decodeControl,
35
+ FrameType,
36
+ } from "./protocol";
37
+
38
+ // Re-export types
39
+ export type { Session, InboxItem, Command, Response };
40
+
41
+ // Re-export constants
42
+ export { PROTOCOL_VERSION, PACKAGE_VERSION, getRouterSocket, getPidFile };
43
+
44
+ /** Status response from server */
45
+ export interface ServerStatus {
46
+ version: string;
47
+ protocol: number;
48
+ pid: number;
49
+ uptime: number;
50
+ sessions: number;
51
+ attached: number;
52
+ }
53
+
54
+ // Terminal reset - RIS (Reset to Initial State) resets everything
55
+ const TERM_RESET = "\x1bc";
56
+
57
+ const SERVER_SCRIPT = `${import.meta.dir}/server.ts`;
58
+
59
+ // CLI args
60
+ const rawArgs = process.argv.slice(2);
61
+ const isTestMode = rawArgs.includes("--test");
62
+ const args = rawArgs.filter(arg => arg !== "--test");
63
+ const cmd = args[0] || "list";
64
+
65
+ if (isTestMode) {
66
+ process.env.TMUX_LITE_SOCKET = "/tmp/tmux-lite-test.sock";
67
+ process.env.TMUX_LITE_SESSION_DIR = "/tmp/tmux-lite-test";
68
+ }
69
+
70
+ const getServerCommand = (): string[] => (
71
+ isTestMode ? ["bun", "run", SERVER_SCRIPT, "--test"] : ["bun", "run", SERVER_SCRIPT]
72
+ );
73
+
74
+ // Check if we're already inside a tmux-lite session
75
+ export function isNested(): boolean {
76
+ return !!process.env.TMUX_LITE;
77
+ }
78
+
79
+ function checkNested(): boolean {
80
+ if (isNested()) {
81
+ console.error("Error: Already inside tmux-lite session " + process.env.TMUX_LITE);
82
+ console.error("Nested sessions are not supported. Detach first with Ctrl+Esc.");
83
+ return true;
84
+ }
85
+ return false;
86
+ }
87
+
88
+ // Check if server is running
89
+ export async function isServerRunning(): Promise<boolean> {
90
+ const routerSocket = getRouterSocket();
91
+ if (!existsSync(routerSocket)) return false;
92
+ try {
93
+ await send({ type: "list" });
94
+ return true;
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ // Start server if not running
101
+ export async function ensureServer(): Promise<void> {
102
+ if (await isServerRunning()) return;
103
+
104
+ spawn({
105
+ cmd: getServerCommand(),
106
+ stdout: "ignore",
107
+ stderr: "ignore",
108
+ });
109
+
110
+ for (let i = 0; i < 30; i++) {
111
+ await Bun.sleep(100);
112
+ if (await isServerRunning()) return;
113
+ }
114
+ throw new Error("Failed to start tmux-lite server");
115
+ }
116
+
117
+ /**
118
+ * Check if a process with given PID is running
119
+ */
120
+ export function isProcessRunning(pid: number): boolean {
121
+ try {
122
+ // Signal 0 doesn't kill - just checks if process exists
123
+ process.kill(pid, 0);
124
+ return true;
125
+ } catch {
126
+ return false;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Get server PID from PID file
132
+ * Returns null if PID file doesn't exist or is invalid
133
+ */
134
+ export function getServerPid(): number | null {
135
+ const pidFile = getPidFile();
136
+ if (!existsSync(pidFile)) return null;
137
+
138
+ try {
139
+ const content = readFileSync(pidFile, "utf-8").trim();
140
+ const pid = parseInt(content, 10);
141
+ if (isNaN(pid) || pid <= 0) return null;
142
+ return pid;
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Clean up stale PID file if process is not running
150
+ * Returns true if cleanup was needed
151
+ */
152
+ export function cleanupStalePidFile(): boolean {
153
+ const pid = getServerPid();
154
+ if (pid === null) return false;
155
+
156
+ if (!isProcessRunning(pid)) {
157
+ // Process is dead, clean up stale files
158
+ const pidFile = getPidFile();
159
+ const routerSocket = getRouterSocket();
160
+ try { unlinkSync(pidFile); } catch {}
161
+ try { unlinkSync(routerSocket); } catch {}
162
+ return true;
163
+ }
164
+ return false;
165
+ }
166
+
167
+ /**
168
+ * Get server version info
169
+ */
170
+ export async function getVersion(): Promise<{ version: string; protocol: number }> {
171
+ await ensureServer();
172
+ const res = await send({ type: "version" });
173
+ if (res.type === "version") {
174
+ return { version: res.version, protocol: res.protocol };
175
+ }
176
+ throw new Error("Unexpected response");
177
+ }
178
+
179
+ /**
180
+ * Get server status (version + stats)
181
+ */
182
+ export async function getStatus(): Promise<ServerStatus> {
183
+ await ensureServer();
184
+ const res = await send({ type: "status" });
185
+ if (res.type === "status") {
186
+ return {
187
+ version: res.version,
188
+ protocol: res.protocol,
189
+ pid: res.pid,
190
+ uptime: res.uptime,
191
+ sessions: res.sessions,
192
+ attached: res.attached,
193
+ };
194
+ }
195
+ throw new Error("Unexpected response");
196
+ }
197
+
198
+ /**
199
+ * Alias for killServer - stops the server daemon
200
+ */
201
+ export const stopServer = killServer;
202
+
203
+ // Send command to server
204
+ export async function send(cmd: Command): Promise<Response> {
205
+ return new Promise(async (resolve, reject) => {
206
+ let buffer: Buffer = Buffer.alloc(0);
207
+ let settled = false;
208
+ let socketRef: Awaited<ReturnType<typeof Bun.connect>> | null = null;
209
+ let socketWriter: ReturnType<typeof createBufferedSocketWriter> | null = null;
210
+
211
+ const fail = (err: Error) => {
212
+ if (settled) return;
213
+ settled = true;
214
+ socketRef?.end();
215
+ reject(err);
216
+ };
217
+
218
+ try {
219
+ const routerSocket = getRouterSocket();
220
+ const socket = await Bun.connect({
221
+ unix: routerSocket,
222
+ socket: {
223
+ drain() {
224
+ socketWriter?.flush();
225
+ },
226
+ data(socket, data) {
227
+ if (settled) return;
228
+ buffer = Buffer.concat([buffer, Buffer.from(data)]);
229
+ let decoded;
230
+ try {
231
+ decoded = decodeRouterMessages(buffer);
232
+ } catch (err) {
233
+ fail(err instanceof Error ? err : new Error("Invalid response"));
234
+ return;
235
+ }
236
+ buffer = decoded.remaining as Buffer;
237
+ if (decoded.messages.length > 0) {
238
+ settled = true;
239
+ resolve(decoded.messages[0] as Response);
240
+ socket.end();
241
+ }
242
+ },
243
+ close() {
244
+ if (!settled) {
245
+ fail(new Error("Connection closed before response"));
246
+ }
247
+ },
248
+ error(_, e) { fail(e); },
249
+ connectError(_, e) { fail(e); }
250
+ }
251
+ });
252
+ socketRef = socket;
253
+ socketWriter = createBufferedSocketWriter(socket);
254
+ socketWriter.write(encodeRouterMessage(cmd));
255
+ } catch (e) {
256
+ fail(e instanceof Error ? e : new Error(String(e)));
257
+ }
258
+ });
259
+ }
260
+
261
+ // === API convenience functions ===
262
+
263
+ export async function listSessions(): Promise<Session[]> {
264
+ await ensureServer();
265
+ const res = await send({ type: "list" });
266
+ if (res.type === "sessions") return res.sessions;
267
+ throw new Error("Unexpected response");
268
+ }
269
+
270
+ export async function createSession(name: string, cwd: string): Promise<Session> {
271
+ await ensureServer();
272
+ const res = await send({ type: "new", name, cwd });
273
+ if (res.type === "session") return res.session;
274
+ if (res.type === "error") throw new Error(res.message);
275
+ throw new Error("Unexpected response");
276
+ }
277
+
278
+ export async function killSession(id: string): Promise<void> {
279
+ await ensureServer();
280
+ const res = await send({ type: "kill", id });
281
+ if (res.type === "error") throw new Error(res.message);
282
+ }
283
+
284
+ export async function killServer(): Promise<void> {
285
+ if (!(await isServerRunning())) return;
286
+ await send({ type: "kill-server" });
287
+ }
288
+
289
+ export async function getInbox(): Promise<InboxItem[]> {
290
+ await ensureServer();
291
+ const res = await send({ type: "inbox" });
292
+ if (res.type === "inbox") return res.items;
293
+ throw new Error("Unexpected response");
294
+ }
295
+
296
+ export async function getUnreadCount(): Promise<number> {
297
+ const items = await getInbox();
298
+ return items.filter(i => !i.read).length;
299
+ }
300
+
301
+ export async function clearInbox(id?: string): Promise<void> {
302
+ await ensureServer();
303
+ await send({ type: "inbox-clear", id });
304
+ }
305
+
306
+ export async function markInboxRead(id: string): Promise<void> {
307
+ await ensureServer();
308
+ await send({ type: "inbox-read", id });
309
+ }
310
+
311
+ // Format session for display
312
+ function formatSession(s: Session): string {
313
+ const age = Math.floor((Date.now() - s.createdAt) / 1000);
314
+ const ageStr = age < 60 ? `${age}s` : age < 3600 ? `${Math.floor(age/60)}m` : `${Math.floor(age/3600)}h`;
315
+ const status = s.attached ? "\x1b[32m●\x1b[0m" : "\x1b[90m○\x1b[0m";
316
+ const title = s.processTitle ? ` \x1b[33m[${s.processTitle}]\x1b[0m` : "";
317
+ return `${status} ${s.id}: ${s.name} (${ageStr})${title} ${s.cwd}`;
318
+ }
319
+
320
+ // Ctrl+Esc sequences (different terminals send different formats)
321
+ const CTRL_ESC_CSI_U = Buffer.from([0x1b, 0x5b, 0x32, 0x37, 0x3b, 0x35, 0x75]); // ESC [ 27;5u
322
+ const CTRL_ESC_XTERM = Buffer.from([0x1b, 0x5b, 0x32, 0x37, 0x3b, 0x35, 0x3b, 0x32, 0x37, 0x7e]); // ESC [ 27;5;27 ~
323
+ const BRACKETED_PASTE_START = Buffer.from([0x1b, 0x5b, 0x32, 0x30, 0x30, 0x7e]); // ESC [ 200 ~
324
+ const BRACKETED_PASTE_END = Buffer.from([0x1b, 0x5b, 0x32, 0x30, 0x31, 0x7e]); // ESC [ 201 ~
325
+
326
+ function containsCtrlEsc(buf: Buffer): number {
327
+ const idx1 = buf.indexOf(CTRL_ESC_CSI_U);
328
+ const idx2 = buf.indexOf(CTRL_ESC_XTERM);
329
+ if (idx1 === -1) return idx2;
330
+ if (idx2 === -1) return idx1;
331
+ return Math.min(idx1, idx2);
332
+ }
333
+
334
+ export type AttachResult =
335
+ | { type: "detached" }
336
+ | { type: "exited"; code: number }
337
+ | { type: "kicked" }
338
+ | { type: "error"; message: string };
339
+
340
+ /**
341
+ * Attach to a session interactively.
342
+ * Takes over stdin/stdout. Returns when session ends or user detaches.
343
+ * @param session Session to attach to
344
+ * @param quiet If true, don't print attach/detach messages
345
+ */
346
+ export async function attach(session: Session, quiet: boolean = false): Promise<AttachResult> {
347
+ if (!quiet) {
348
+ console.log(`Attaching to ${session.name}...`);
349
+ console.log("Ctrl+Esc to detach\n");
350
+ }
351
+
352
+ return new Promise(async (resolve) => {
353
+ let buffer = Buffer.alloc(0);
354
+ let pendingSeq = Buffer.alloc(0);
355
+ let inBracketedPaste = false;
356
+ let resolved = false;
357
+ let stdinListener: ((chunk: Buffer) => void) | null = null;
358
+ let socket: Awaited<ReturnType<typeof Bun.connect>> | null = null;
359
+ let socketWriter: ReturnType<typeof createBufferedSocketWriter> | null = null;
360
+ let onResize: (() => void) | null = null;
361
+ let lastSize = { cols: 0, rows: 0 };
362
+
363
+ const cleanup = (result: AttachResult) => {
364
+ if (resolved) return;
365
+ resolved = true;
366
+ if (stdinListener) {
367
+ process.stdin.removeListener("data", stdinListener);
368
+ }
369
+ process.stdin.setRawMode(false);
370
+ process.stdin.pause();
371
+ process.stdout.write(TERM_RESET);
372
+ if (onResize) {
373
+ process.removeListener("SIGWINCH", onResize);
374
+ }
375
+ socket = null;
376
+ socketWriter = null;
377
+ if (!quiet) {
378
+ if (result.type === "detached") console.log("\n[detached]");
379
+ else if (result.type === "exited") console.log(`\n[exited: ${result.code}]`);
380
+ else if (result.type === "kicked") console.log("\n[kicked - another client took over]");
381
+ else if (result.type === "error") console.error("\n[error]", result.message);
382
+ }
383
+ resolve(result);
384
+ };
385
+
386
+ const getTermSize = () => {
387
+ let cols = process.stdout.columns || 0;
388
+ let rows = process.stdout.rows || 0;
389
+ if (cols <= 0 || rows <= 0) {
390
+ const size = (process.stdout as { getWindowSize?: () => number[] }).getWindowSize?.();
391
+ if (Array.isArray(size) && size.length >= 2) {
392
+ cols = size[0];
393
+ rows = size[1];
394
+ }
395
+ }
396
+ return {
397
+ cols: cols > 0 ? cols : 80,
398
+ rows: rows > 0 ? rows : 24,
399
+ };
400
+ };
401
+
402
+ const sendResize = (force = false) => {
403
+ if (!socket) return;
404
+ const { cols, rows } = getTermSize();
405
+ if (!force && cols === lastSize.cols && rows === lastSize.rows) {
406
+ return;
407
+ }
408
+ lastSize = { cols, rows };
409
+ const frame = encodeControl({ type: "resize", cols, rows });
410
+ if (socketWriter) socketWriter.write(frame);
411
+ else socket.write(frame);
412
+ };
413
+
414
+ const sendAttachInit = () => {
415
+ if (!socket) return;
416
+ const { cols, rows } = getTermSize();
417
+ const frame = encodeControl({ type: "attach-init", cols, rows, clientType: "cli" });
418
+ if (socketWriter) socketWriter.write(frame);
419
+ else socket.write(frame);
420
+ };
421
+
422
+ socket = await Bun.connect({
423
+ unix: session.socketPath,
424
+ socket: {
425
+ drain() {
426
+ socketWriter?.flush();
427
+ },
428
+ data(socket, data) {
429
+ let buf = Buffer.from(data);
430
+
431
+ if (buffer.length > 0) {
432
+ buf = Buffer.concat([buffer, buf]);
433
+ }
434
+
435
+ // Parse frames from the buffer
436
+ let frames;
437
+ let remaining;
438
+ try {
439
+ const result = parseFrames(buf);
440
+ frames = result.frames;
441
+ remaining = result.remaining;
442
+ } catch (err) {
443
+ // Protocol error - likely desync or corrupted data
444
+ const msg = err instanceof Error ? err.message : 'Frame parse error';
445
+ console.error(`[attach] Frame parse error: ${msg}`);
446
+ cleanup({ type: "error", message: msg });
447
+ return;
448
+ }
449
+ buffer = Buffer.from(remaining);
450
+
451
+ for (const frame of frames) {
452
+ if (frame.type === FrameType.CONTROL) {
453
+ const event = decodeControl(frame.payload) as SessionEvent;
454
+
455
+ if (event.type === "attached") {
456
+ // Send a single resize to ensure proper dimensions
457
+ sendResize(true);
458
+ } else if (event.type === "exited") {
459
+ cleanup({ type: "exited", code: event.code });
460
+ return;
461
+ } else if (event.type === "kicked") {
462
+ cleanup({ type: "kicked" });
463
+ return;
464
+ }
465
+ } else if (frame.type === FrameType.PTY) {
466
+ process.stdout.write(frame.payload);
467
+ }
468
+ }
469
+ },
470
+
471
+ close() {
472
+ cleanup({ type: "detached" });
473
+ },
474
+
475
+ error(_, e) {
476
+ cleanup({ type: "error", message: e.message });
477
+ }
478
+ }
479
+ });
480
+ socketWriter = createBufferedSocketWriter(socket);
481
+
482
+ // Initial resize
483
+ sendAttachInit();
484
+ sendResize(true);
485
+
486
+ onResize = () => {
487
+ sendResize();
488
+ };
489
+ process.on("SIGWINCH", onResize);
490
+ process.stdin.setRawMode(true);
491
+ process.stdin.resume();
492
+
493
+ // Forward stdin with Ctrl+Esc detection
494
+ stdinListener = (chunk: Buffer) => {
495
+ const combined = pendingSeq.length > 0 ? Buffer.concat([pendingSeq, chunk]) : chunk;
496
+ pendingSeq = Buffer.alloc(0);
497
+ const out: Buffer[] = [];
498
+ let offset = 0;
499
+
500
+ const flushOut = () => {
501
+ if (out.length > 0 && socket) {
502
+ const frame = encodePTY(Buffer.concat(out));
503
+ if (socketWriter) socketWriter.write(frame);
504
+ else socket.write(frame);
505
+ out.length = 0;
506
+ }
507
+ };
508
+
509
+ const getSequences = () => (
510
+ inBracketedPaste
511
+ ? [BRACKETED_PASTE_START, BRACKETED_PASTE_END]
512
+ : [BRACKETED_PASTE_START, BRACKETED_PASTE_END, CTRL_ESC_CSI_U, CTRL_ESC_XTERM]
513
+ );
514
+
515
+ while (offset < combined.length) {
516
+ if (combined[offset] !== 0x1b) {
517
+ const nextEsc = combined.indexOf(0x1b, offset + 1);
518
+ if (nextEsc === -1) {
519
+ out.push(combined.subarray(offset));
520
+ offset = combined.length;
521
+ } else {
522
+ out.push(combined.subarray(offset, nextEsc));
523
+ offset = nextEsc;
524
+ }
525
+ continue;
526
+ }
527
+
528
+ const sequences = getSequences();
529
+ let matched: Buffer | null = null;
530
+ for (const seq of sequences) {
531
+ if (combined.length - offset >= seq.length &&
532
+ combined.subarray(offset, offset + seq.length).equals(seq)) {
533
+ matched = seq;
534
+ break;
535
+ }
536
+ }
537
+
538
+ if (matched) {
539
+ if (matched === CTRL_ESC_CSI_U || matched === CTRL_ESC_XTERM) {
540
+ flushOut();
541
+ if (socket) {
542
+ const frame = encodeControl({ type: "detach" });
543
+ if (socketWriter) socketWriter.write(frame);
544
+ else socket.write(frame);
545
+ }
546
+ cleanup({ type: "detached" });
547
+ return;
548
+ }
549
+
550
+ out.push(combined.subarray(offset, offset + matched.length));
551
+ if (matched === BRACKETED_PASTE_START) {
552
+ inBracketedPaste = true;
553
+ } else if (matched === BRACKETED_PASTE_END) {
554
+ inBracketedPaste = false;
555
+ }
556
+ offset += matched.length;
557
+ continue;
558
+ }
559
+
560
+ let possiblePrefix = false;
561
+ for (const seq of sequences) {
562
+ const remaining = combined.length - offset;
563
+ if (remaining < seq.length &&
564
+ seq.subarray(0, remaining).equals(combined.subarray(offset))) {
565
+ possiblePrefix = true;
566
+ break;
567
+ }
568
+ }
569
+
570
+ if (possiblePrefix) {
571
+ pendingSeq = Buffer.from(combined.subarray(offset));
572
+ break;
573
+ }
574
+
575
+ out.push(combined.subarray(offset, offset + 1));
576
+ offset += 1;
577
+ }
578
+
579
+ flushOut();
580
+ };
581
+
582
+ process.stdin.on("data", stdinListener);
583
+ });
584
+ }
585
+
586
+ // Handle attach result and exit with appropriate code
587
+ function handleAttachResult(result: AttachResult): void {
588
+ if (result.type === "exited") {
589
+ process.exit(result.code);
590
+ } else if (result.type === "error") {
591
+ process.exit(1);
592
+ }
593
+ // detached and kicked exit cleanly
594
+ process.exit(0);
595
+ }
596
+
597
+ // Main
598
+ async function main() {
599
+ // Start server if not running
600
+ if (!(await isServerRunning())) {
601
+ if (cmd === "kill-server") {
602
+ console.log("Server not running");
603
+ return;
604
+ }
605
+ console.log("Starting server...");
606
+ spawn({
607
+ cmd: getServerCommand(),
608
+ stdout: "inherit",
609
+ stderr: "inherit",
610
+ });
611
+ await Bun.sleep(300);
612
+ if (!(await isServerRunning())) {
613
+ console.error("Failed to start server");
614
+ process.exit(1);
615
+ }
616
+ }
617
+
618
+ switch (cmd) {
619
+ case "new": {
620
+ if (checkNested()) process.exit(1);
621
+ const name = args[1];
622
+ const res = await send({ type: "new", name, cwd: process.cwd() });
623
+ if (res.type === "session") {
624
+ const result = await attach(res.session);
625
+ handleAttachResult(result);
626
+ } else if (res.type === "error") {
627
+ console.error("Error:", res.message);
628
+ }
629
+ break;
630
+ }
631
+
632
+ case "a":
633
+ case "attach": {
634
+ if (checkNested()) process.exit(1);
635
+ const id = args[1];
636
+ if (id) {
637
+ const res = await send({ type: "attach", id, force: args.includes("-f") });
638
+ if (res.type === "session") {
639
+ const result = await attach(res.session);
640
+ handleAttachResult(result);
641
+ } else if (res.type === "already-attached") {
642
+ console.log(`Session ${id} is attached elsewhere.\n`);
643
+ const choice = await select({
644
+ message: "What to do?",
645
+ choices: [
646
+ { value: "force", name: "Take over" },
647
+ { value: "cancel", name: "Cancel" },
648
+ ]
649
+ });
650
+ if (choice === "force") {
651
+ const res2 = await send({ type: "attach", id, force: true });
652
+ if (res2.type === "session") {
653
+ const result = await attach(res2.session);
654
+ handleAttachResult(result);
655
+ }
656
+ }
657
+ } else if (res.type === "error") {
658
+ console.error("Error:", res.message);
659
+ }
660
+ } else {
661
+ // No ID - show picker
662
+ const res = await send({ type: "list" });
663
+ if (res.type === "sessions") {
664
+ if (res.sessions.length === 0) {
665
+ console.log("No sessions. Create with: tl new");
666
+ } else {
667
+ const choice = await select({
668
+ message: "Select session:",
669
+ choices: res.sessions.map(s => ({
670
+ value: s.id,
671
+ name: formatSession(s)
672
+ }))
673
+ });
674
+ const res2 = await send({ type: "attach", id: choice });
675
+ if (res2.type === "session") {
676
+ const result = await attach(res2.session);
677
+ handleAttachResult(result);
678
+ } else if (res2.type === "already-attached") {
679
+ const force = await select({
680
+ message: "Session attached. Take over?",
681
+ choices: [
682
+ { value: true, name: "Yes" },
683
+ { value: false, name: "No" },
684
+ ]
685
+ });
686
+ if (force) {
687
+ const res3 = await send({ type: "attach", id: choice, force: true });
688
+ if (res3.type === "session") {
689
+ const result = await attach(res3.session);
690
+ handleAttachResult(result);
691
+ }
692
+ }
693
+ }
694
+ }
695
+ }
696
+ }
697
+ break;
698
+ }
699
+
700
+ case "ls":
701
+ case "list": {
702
+ const res = await send({ type: "list" });
703
+ if (res.type === "sessions") {
704
+ if (res.sessions.length === 0) {
705
+ console.log("No sessions");
706
+ } else {
707
+ console.log("Sessions:");
708
+ for (const s of res.sessions) {
709
+ console.log(" " + formatSession(s));
710
+ }
711
+ }
712
+ }
713
+ break;
714
+ }
715
+
716
+ case "kill": {
717
+ if (checkNested()) process.exit(1);
718
+ const id = args[1];
719
+ if (!id) {
720
+ console.error("Usage: tl kill <id>");
721
+ process.exit(1);
722
+ }
723
+ const res = await send({ type: "kill", id });
724
+ if (res.type === "ok") {
725
+ console.log(`Killed ${id}`);
726
+ } else if (res.type === "error") {
727
+ console.error("Error:", res.message);
728
+ }
729
+ break;
730
+ }
731
+
732
+ case "kill-server": {
733
+ if (checkNested()) process.exit(1);
734
+ await send({ type: "kill-server" });
735
+ console.log("Server stopped");
736
+ break;
737
+ }
738
+
739
+ case "inbox": {
740
+ const res = await send({ type: "inbox" });
741
+ if (res.type === "inbox") {
742
+ if (res.items.length === 0) {
743
+ console.log("Inbox empty");
744
+ } else {
745
+ console.log("Inbox:");
746
+ for (const item of res.items) {
747
+ const icon = item.type === 'exit'
748
+ ? (item.exitCode === 0 ? '✓' : '✖')
749
+ : '🔔';
750
+ const status = item.read ? '' : ' (unread)';
751
+ const time = new Date(item.timestamp).toLocaleTimeString();
752
+ console.log(` ${icon} [${time}] ${item.sessionName}${status}`);
753
+ // Indent context lines
754
+ const lines = item.context.split('\n').slice(0, 3);
755
+ for (const line of lines) {
756
+ console.log(` ${line}`);
757
+ }
758
+ }
759
+ }
760
+ }
761
+ break;
762
+ }
763
+
764
+ case "inbox-clear": {
765
+ const id = args[1];
766
+ await send({ type: "inbox-clear", id });
767
+ console.log(id ? `Cleared inbox item ${id}` : "Inbox cleared");
768
+ break;
769
+ }
770
+
771
+ default:
772
+ console.log(`
773
+ tmux-lite
774
+
775
+ Commands:
776
+ tl new [name] Create session
777
+ tl attach [id] Attach (picker if no id)
778
+ tl list List sessions
779
+ tl kill <id> Kill session
780
+ tl kill-server Stop server
781
+ tl inbox Show inbox (bells, exits)
782
+ tl inbox-clear Clear inbox
783
+
784
+ In session:
785
+ Ctrl+Esc Detach
786
+ `);
787
+ }
788
+ }
789
+
790
+ // Only run CLI when executed directly, not when imported as a module
791
+ if (import.meta.main) {
792
+ main().catch(e => {
793
+ console.error(e.message);
794
+ process.exit(1);
795
+ });
796
+ }