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,581 @@
1
+ /**
2
+ * Remote session handler - processes browse and PTY commands
3
+ *
4
+ * Handles the encrypted messages between client and machine after X3DH handshake.
5
+ */
6
+
7
+ import { createFrame, openFrame } from "../tmux-lite/crypto/frames";
8
+ import { scanWorkspaces } from "./workspace-scanner";
9
+ import {
10
+ parseRemoteMessage,
11
+ serializeRemoteMessage,
12
+ type ClientToMachineMessage,
13
+ type MachineToClientMessage,
14
+ type SessionInfo,
15
+ } from "./protocol";
16
+ import type { SessionKeys, AccessType } from "../../types/identity.js";
17
+
18
+ // Import tmux-lite API for session management
19
+ import {
20
+ listSessions,
21
+ createSession,
22
+ killSession,
23
+ isServerRunning,
24
+ ensureServer,
25
+ getInbox,
26
+ clearInbox,
27
+ markInboxRead,
28
+ type Session,
29
+ } from "../tmux-lite/cli";
30
+
31
+ // Import project loading
32
+ import { loadProjects } from "../../tui/state";
33
+
34
+ // Import workspace removal
35
+ import { removeWorktree, deleteLocalBranch, getWorktreeInfo } from "../../core/git";
36
+ import { getProjectWorkspacesDir, getProjectBaseDir } from "../../core/config";
37
+
38
+ /**
39
+ * Session state for a connected client
40
+ */
41
+ export type ClientState = "browsing" | "attached";
42
+
43
+ export interface RemoteClientSession {
44
+ connectionId: string;
45
+ state: ClientState;
46
+ sessionKeys: SessionKeys;
47
+ /** Access type granted to this client */
48
+ accessType?: AccessType;
49
+ /** For session-invite: the specific session ID access was granted to */
50
+ grantedSessionId?: string;
51
+ /** Attached tmux-lite session ID (set after attach_session) */
52
+ attachedSessionId?: string;
53
+ /** Path to tmux-lite session socket (set after attach_session) */
54
+ sessionSocketPath?: string;
55
+ }
56
+
57
+ // ============================================================================
58
+ // Permission Helpers
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Check if access type grants management permission
63
+ */
64
+ function canManage(accessType: AccessType | undefined): boolean {
65
+ return accessType === 'full';
66
+ }
67
+
68
+ /**
69
+ * Check if client can attach to a specific session
70
+ */
71
+ function canAttachSession(
72
+ accessType: AccessType | undefined,
73
+ grantedSessionId: string | undefined,
74
+ targetSessionId: string
75
+ ): boolean {
76
+ if (accessType === 'full') return true;
77
+ if (accessType === 'session-invite') {
78
+ return grantedSessionId === targetSessionId;
79
+ }
80
+ return false;
81
+ }
82
+
83
+ /**
84
+ * Remote session handler
85
+ */
86
+ export class RemoteSessionHandler {
87
+ private tmuxLiteAvailable = false;
88
+
89
+ /**
90
+ * Initialize - check if tmux-lite is available
91
+ */
92
+ async initialize(): Promise<void> {
93
+ try {
94
+ this.tmuxLiteAvailable = await isServerRunning();
95
+ if (!this.tmuxLiteAvailable) {
96
+ // Try to start the server
97
+ await ensureServer();
98
+ this.tmuxLiteAvailable = true;
99
+ }
100
+ } catch (e) {
101
+ console.warn("[remote-session] tmux-lite not available:", e);
102
+ this.tmuxLiteAvailable = false;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Handle an encrypted message from a client
108
+ *
109
+ * @param session - Client session info
110
+ * @param encryptedData - Encrypted frame data
111
+ * @param sendResponse - Callback to send encrypted response
112
+ */
113
+ async handleMessage(
114
+ session: RemoteClientSession,
115
+ encryptedData: Uint8Array,
116
+ sendResponse: (data: Uint8Array) => void
117
+ ): Promise<void> {
118
+ try {
119
+ // Decrypt the frame
120
+ const frame = await openFrame(encryptedData, session.sessionKeys.receiveKey);
121
+ if (!frame) {
122
+ console.error("[remote-session] Failed to decrypt frame");
123
+ return;
124
+ }
125
+
126
+ // Parse as JSON message
127
+ const json = new TextDecoder().decode(frame.data);
128
+ const msg = parseRemoteMessage(json);
129
+
130
+ if (!msg) {
131
+ console.error("[remote-session] Failed to parse message");
132
+ return;
133
+ }
134
+
135
+ // Handle based on message type
136
+ await this.processMessage(session, msg as ClientToMachineMessage, sendResponse);
137
+ } catch (e) {
138
+ console.error("[remote-session] Error handling message:", e);
139
+ await this.sendError(session, sendResponse, "INTERNAL_ERROR", "Failed to process message");
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Process a client message
145
+ */
146
+ private async processMessage(
147
+ session: RemoteClientSession,
148
+ msg: ClientToMachineMessage,
149
+ sendResponse: (data: Uint8Array) => void
150
+ ): Promise<void> {
151
+ switch (msg.type) {
152
+ case "list_workspaces":
153
+ await this.handleListWorkspaces(session, sendResponse);
154
+ break;
155
+
156
+ case "list_sessions":
157
+ await this.handleListSessions(session, msg.workspaceId, sendResponse);
158
+ break;
159
+
160
+ case "attach_session":
161
+ // Permission check for attach_session is done in handleAttachSession
162
+ // because it depends on whether creating new session or attaching existing
163
+ await this.handleAttachSession(session, msg, sendResponse);
164
+ break;
165
+
166
+ // Note: resize, detach, and pty_input are handled in attached mode
167
+ // via client-session-manager using tmux-lite's SessionCtrl protocol,
168
+ // not through this JSON-RPC handler.
169
+
170
+ case "list_projects":
171
+ await this.handleListProjects(session, sendResponse);
172
+ break;
173
+
174
+ case "kill_session":
175
+ // Security: Requires management permission
176
+ if (!canManage(session.accessType)) {
177
+ await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to kill sessions");
178
+ return;
179
+ }
180
+ await this.handleKillSession(session, msg.sessionId, sendResponse);
181
+ break;
182
+
183
+ case "delete_workspace":
184
+ // Security: Requires management permission
185
+ if (!canManage(session.accessType)) {
186
+ await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to delete workspaces");
187
+ return;
188
+ }
189
+ await this.handleDeleteWorkspace(session, msg.projectName, msg.workspaceId, sendResponse);
190
+ break;
191
+
192
+ case "get_inbox":
193
+ await this.handleGetInbox(session, sendResponse);
194
+ break;
195
+
196
+ case "clear_inbox":
197
+ await this.handleClearInbox(session, msg.id, sendResponse);
198
+ break;
199
+
200
+ case "mark_inbox_read":
201
+ await this.handleMarkInboxRead(session, msg.id, sendResponse);
202
+ break;
203
+
204
+ default: {
205
+ // Exhaustiveness check - log unknown message types
206
+ const unknownMsg = msg as { type: string };
207
+ console.warn("[remote-session] Unknown message type:", unknownMsg.type);
208
+ }
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Handle list_workspaces request
214
+ */
215
+ private async handleListWorkspaces(
216
+ session: RemoteClientSession,
217
+ sendResponse: (data: Uint8Array) => void
218
+ ): Promise<void> {
219
+ const workspaces = await scanWorkspaces();
220
+
221
+ // Add session counts from tmux-lite
222
+ if (this.tmuxLiteAvailable) {
223
+ try {
224
+ const sessions = await listSessions();
225
+ for (const workspace of workspaces) {
226
+ // Count sessions for this workspace by matching cwd.
227
+ // Note: Session cwd is set once at creation time and does NOT change
228
+ // as users navigate within the shell. This is intentional - we want to
229
+ // show sessions that were *created for* this workspace.
230
+ workspace.sessionCount = sessions.filter(s => s.cwd === workspace.path).length;
231
+ }
232
+ } catch {
233
+ // Ignore errors - just use 0 session counts
234
+ }
235
+ }
236
+
237
+ await this.sendMessage(session, sendResponse, {
238
+ type: "workspace_list",
239
+ workspaces,
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Handle list_sessions request
245
+ */
246
+ private async handleListSessions(
247
+ session: RemoteClientSession,
248
+ workspaceId: string | undefined,
249
+ sendResponse: (data: Uint8Array) => void
250
+ ): Promise<void> {
251
+ let sessions: SessionInfo[] = [];
252
+
253
+ if (this.tmuxLiteAvailable) {
254
+ try {
255
+ const allSessions = await listSessions();
256
+ const workspaces = await scanWorkspaces();
257
+
258
+ // Build a map of workspace path -> workspace info
259
+ const workspacePathMap = new Map(workspaces.map(w => [w.path, w]));
260
+
261
+ sessions = allSessions
262
+ .filter(s => {
263
+ if (!workspaceId) return true;
264
+ // Filter by workspace using cwd matching
265
+ // Note: Session cwd is set once at creation time and does NOT change
266
+ // as users navigate within the shell.
267
+ const ws = workspacePathMap.get(s.cwd);
268
+ return ws?.id === workspaceId;
269
+ })
270
+ .map(s => {
271
+ // Find workspace info by cwd
272
+ const ws = workspacePathMap.get(s.cwd);
273
+ return {
274
+ id: s.id,
275
+ name: s.name,
276
+ workspaceId: ws?.id ?? "unknown",
277
+ attached: s.attached,
278
+ createdAt: s.createdAt,
279
+ processTitle: s.processTitle,
280
+ exitCode: s.exitCode,
281
+ };
282
+ });
283
+ } catch (e) {
284
+ console.error("[remote-session] Failed to list sessions:", e);
285
+ }
286
+ }
287
+
288
+ await this.sendMessage(session, sendResponse, {
289
+ type: "session_list",
290
+ sessions,
291
+ });
292
+ }
293
+
294
+ /**
295
+ * Handle attach_session request
296
+ */
297
+ private async handleAttachSession(
298
+ session: RemoteClientSession,
299
+ msg: { sessionId?: string; workspaceId?: string; sessionName?: string; cols?: number; rows?: number },
300
+ sendResponse: (data: Uint8Array) => void
301
+ ): Promise<void> {
302
+ console.log("[remote-session] handleAttachSession:", JSON.stringify(msg));
303
+
304
+ if (!this.tmuxLiteAvailable) {
305
+ await this.sendError(session, sendResponse, "UNAVAILABLE", "Session manager not available");
306
+ return;
307
+ }
308
+
309
+ try {
310
+ let targetSession: Session | null = null;
311
+
312
+ // If no session ID, create new session in workspace
313
+ if (!msg.sessionId && msg.workspaceId) {
314
+ // Security: Creating new sessions requires full/manage access
315
+ if (!canManage(session.accessType)) {
316
+ await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Requires full access to create sessions");
317
+ return;
318
+ }
319
+
320
+ // Find the workspace path
321
+ const workspaces = await scanWorkspaces();
322
+ const workspace = workspaces.find(w => w.id === msg.workspaceId);
323
+
324
+ if (!workspace) {
325
+ await this.sendError(session, sendResponse, "NOT_FOUND", "Workspace not found");
326
+ return;
327
+ }
328
+
329
+ // Create session name: use provided name or auto-generate
330
+ let sessionName: string;
331
+ if (msg.sessionName) {
332
+ // Use provided name with project:workspace prefix
333
+ sessionName = `${workspace.projectName}:${workspace.id}:${msg.sessionName}`;
334
+ console.log(`[remote-session] Using provided session name: ${sessionName}`);
335
+ } else {
336
+ // Auto-generate: project:workspace:N
337
+ const sessions = await listSessions();
338
+ const existingCount = sessions.filter(s =>
339
+ s.name.startsWith(`${workspace.projectName}:${workspace.id}:`)
340
+ ).length;
341
+ sessionName = `${workspace.projectName}:${workspace.id}:${existingCount + 1}`;
342
+ console.log(`[remote-session] Auto-generated session name: ${sessionName}`);
343
+ }
344
+
345
+ targetSession = await createSession(sessionName, workspace.path);
346
+ console.log(`[remote-session] Created session: ${targetSession.name} (id: ${targetSession.id})`)
347
+ } else if (msg.sessionId) {
348
+ // Security: Check if client can attach to this session
349
+ if (!canAttachSession(session.accessType, session.grantedSessionId, msg.sessionId)) {
350
+ await this.sendError(session, sendResponse, "PERMISSION_DENIED", "Not authorized to attach to this session");
351
+ return;
352
+ }
353
+
354
+ // Find existing session
355
+ const sessions = await listSessions();
356
+ targetSession = sessions.find(s => s.id === msg.sessionId) ?? null;
357
+ }
358
+
359
+ if (!targetSession) {
360
+ await this.sendError(session, sendResponse, "NOT_FOUND", "Session not found");
361
+ return;
362
+ }
363
+
364
+ session.state = "attached";
365
+ session.attachedSessionId = targetSession.id;
366
+ session.sessionSocketPath = targetSession.socketPath;
367
+
368
+ // Send confirmation - ClientSessionManager will connect to the socket
369
+ await this.sendMessage(session, sendResponse, {
370
+ type: "attached",
371
+ sessionId: targetSession.id,
372
+ sessionName: targetSession.name,
373
+ cols: msg.cols ?? 80,
374
+ rows: msg.rows ?? 24,
375
+ });
376
+ } catch (e) {
377
+ console.error("[remote-session] Failed to attach session:", e);
378
+ await this.sendError(session, sendResponse, "ATTACH_FAILED", "Failed to attach to session");
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Handle list_projects request
384
+ */
385
+ private async handleListProjects(
386
+ session: RemoteClientSession,
387
+ sendResponse: (data: Uint8Array) => void
388
+ ): Promise<void> {
389
+ try {
390
+ const projects = loadProjects();
391
+ await this.sendMessage(session, sendResponse, {
392
+ type: "project_list",
393
+ projects: projects.map(p => ({
394
+ name: p.name,
395
+ repository: p.repository,
396
+ workspaceCount: p.workspaceCount,
397
+ isCurrent: p.isCurrent,
398
+ })),
399
+ });
400
+ } catch (e) {
401
+ console.error("[remote-session] Failed to list projects:", e);
402
+ await this.sendError(session, sendResponse, "LIST_FAILED", "Failed to list projects");
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Handle kill_session request
408
+ */
409
+ private async handleKillSession(
410
+ session: RemoteClientSession,
411
+ sessionId: string,
412
+ sendResponse: (data: Uint8Array) => void
413
+ ): Promise<void> {
414
+ if (!this.tmuxLiteAvailable) {
415
+ await this.sendError(session, sendResponse, "UNAVAILABLE", "Session manager not available");
416
+ return;
417
+ }
418
+
419
+ try {
420
+ // Look up the session's workspaceId before killing
421
+ const sessions = await listSessions();
422
+ const workspaces = await scanWorkspaces();
423
+ const workspacePathMap = new Map(workspaces.map(w => [w.path, w]));
424
+ const targetSession = sessions.find(s => s.id === sessionId);
425
+ const workspace = targetSession ? workspacePathMap.get(targetSession.cwd) : undefined;
426
+ const workspaceId = workspace?.id ?? "unknown";
427
+
428
+ await killSession(sessionId);
429
+ // Wait a bit for the server to process the kill
430
+ await new Promise(resolve => setTimeout(resolve, 100));
431
+ await this.sendMessage(session, sendResponse, {
432
+ type: "session_killed",
433
+ sessionId,
434
+ workspaceId,
435
+ });
436
+ } catch (e) {
437
+ console.error("[remote-session] Failed to kill session:", e);
438
+ await this.sendError(session, sendResponse, "KILL_FAILED", "Failed to kill session");
439
+ }
440
+ }
441
+
442
+ /**
443
+ * Handle delete_workspace request
444
+ */
445
+ private async handleDeleteWorkspace(
446
+ session: RemoteClientSession,
447
+ projectName: string,
448
+ workspaceId: string,
449
+ sendResponse: (data: Uint8Array) => void
450
+ ): Promise<void> {
451
+ try {
452
+ const workspacesDir = getProjectWorkspacesDir(projectName);
453
+ const baseDir = getProjectBaseDir(projectName);
454
+ const workspacePath = `${workspacesDir}/${workspaceId}`;
455
+
456
+ // Get workspace info for branch name
457
+ const info = await getWorktreeInfo(workspacePath);
458
+ if (!info) {
459
+ await this.sendError(session, sendResponse, "NOT_FOUND", "Workspace not found");
460
+ return;
461
+ }
462
+
463
+ // Remove worktree
464
+ await removeWorktree(baseDir, workspacePath, true);
465
+
466
+ // Try to delete the local branch
467
+ try {
468
+ await deleteLocalBranch(baseDir, info.branch, true);
469
+ } catch {
470
+ // Branch deletion is best-effort
471
+ }
472
+
473
+ await this.sendMessage(session, sendResponse, {
474
+ type: "workspace_deleted",
475
+ workspaceId,
476
+ });
477
+ } catch (e) {
478
+ console.error("[remote-session] Failed to delete workspace:", e);
479
+ await this.sendError(session, sendResponse, "DELETE_FAILED", "Failed to delete workspace");
480
+ }
481
+ }
482
+
483
+ /**
484
+ * Handle get_inbox request
485
+ */
486
+ private async handleGetInbox(
487
+ session: RemoteClientSession,
488
+ sendResponse: (data: Uint8Array) => void
489
+ ): Promise<void> {
490
+ try {
491
+ const items = await getInbox();
492
+ const unreadCount = items.filter(i => !i.read).length;
493
+ await this.sendMessage(session, sendResponse, {
494
+ type: "inbox_list",
495
+ items,
496
+ unreadCount,
497
+ });
498
+ } catch (e) {
499
+ console.error("[remote-session] Failed to get inbox:", e);
500
+ await this.sendError(session, sendResponse, "INBOX_FAILED", "Failed to get inbox");
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Handle clear_inbox request
506
+ */
507
+ private async handleClearInbox(
508
+ session: RemoteClientSession,
509
+ id: string | undefined,
510
+ sendResponse: (data: Uint8Array) => void
511
+ ): Promise<void> {
512
+ try {
513
+ await clearInbox(id);
514
+ await this.sendMessage(session, sendResponse, {
515
+ type: "inbox_cleared",
516
+ id,
517
+ });
518
+ } catch (e) {
519
+ console.error("[remote-session] Failed to clear inbox:", e);
520
+ await this.sendError(session, sendResponse, "INBOX_FAILED", "Failed to clear inbox");
521
+ }
522
+ }
523
+
524
+ /**
525
+ * Handle mark_inbox_read request
526
+ */
527
+ private async handleMarkInboxRead(
528
+ session: RemoteClientSession,
529
+ id: string,
530
+ sendResponse: (data: Uint8Array) => void
531
+ ): Promise<void> {
532
+ try {
533
+ await markInboxRead(id);
534
+ await this.sendMessage(session, sendResponse, {
535
+ type: "inbox_marked_read",
536
+ id,
537
+ });
538
+ } catch (e) {
539
+ console.error("[remote-session] Failed to mark inbox read:", e);
540
+ await this.sendError(session, sendResponse, "INBOX_FAILED", "Failed to mark inbox item as read");
541
+ }
542
+ }
543
+
544
+ /**
545
+ * Send an encrypted message to client
546
+ */
547
+ private async sendMessage(
548
+ session: RemoteClientSession,
549
+ sendResponse: (data: Uint8Array) => void,
550
+ msg: MachineToClientMessage
551
+ ): Promise<void> {
552
+ const json = serializeRemoteMessage(msg);
553
+ const data = new TextEncoder().encode(json);
554
+ const frame = await createFrame(0, data, session.sessionKeys.sendKey);
555
+ sendResponse(frame);
556
+ }
557
+
558
+ /**
559
+ * Send an error message to client
560
+ */
561
+ private async sendError(
562
+ session: RemoteClientSession,
563
+ sendResponse: (data: Uint8Array) => void,
564
+ code: string,
565
+ message: string
566
+ ): Promise<void> {
567
+ await this.sendMessage(session, sendResponse, {
568
+ type: "error",
569
+ code,
570
+ message,
571
+ });
572
+ }
573
+
574
+ /**
575
+ * Cleanup
576
+ */
577
+ async cleanup(): Promise<void> {
578
+ // No persistent connection to clean up with the new API
579
+ this.tmuxLiteAvailable = false;
580
+ }
581
+ }