@aoagents/ao-web 0.2.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 (230) hide show
  1. package/.next/BUILD_ID +1 -0
  2. package/.next/app-build-manifest.json +293 -0
  3. package/.next/app-path-routes-manifest.json +33 -0
  4. package/.next/build-manifest.json +33 -0
  5. package/.next/export-marker.json +6 -0
  6. package/.next/images-manifest.json +58 -0
  7. package/.next/next-minimal-server.js.nft.json +1 -0
  8. package/.next/next-server.js.nft.json +1 -0
  9. package/.next/package.json +1 -0
  10. package/.next/prerender-manifest.json +172 -0
  11. package/.next/react-loadable-manifest.json +47 -0
  12. package/.next/required-server-files.json +330 -0
  13. package/.next/routes-manifest.json +195 -0
  14. package/.next/server/app/_not-found/page.js +2 -0
  15. package/.next/server/app/_not-found/page.js.nft.json +1 -0
  16. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
  17. package/.next/server/app/_not-found.html +1 -0
  18. package/.next/server/app/_not-found.meta +8 -0
  19. package/.next/server/app/_not-found.rsc +23 -0
  20. package/.next/server/app/api/backlog/route.js +1 -0
  21. package/.next/server/app/api/backlog/route.js.nft.json +1 -0
  22. package/.next/server/app/api/backlog/route_client-reference-manifest.js +1 -0
  23. package/.next/server/app/api/events/route.js +9 -0
  24. package/.next/server/app/api/events/route.js.nft.json +1 -0
  25. package/.next/server/app/api/events/route_client-reference-manifest.js +1 -0
  26. package/.next/server/app/api/issues/route.js +1 -0
  27. package/.next/server/app/api/issues/route.js.nft.json +1 -0
  28. package/.next/server/app/api/issues/route_client-reference-manifest.js +1 -0
  29. package/.next/server/app/api/observability/route.js +1 -0
  30. package/.next/server/app/api/observability/route.js.nft.json +1 -0
  31. package/.next/server/app/api/observability/route_client-reference-manifest.js +1 -0
  32. package/.next/server/app/api/orchestrators/route.js +1 -0
  33. package/.next/server/app/api/orchestrators/route.js.nft.json +1 -0
  34. package/.next/server/app/api/orchestrators/route_client-reference-manifest.js +1 -0
  35. package/.next/server/app/api/projects/route.js +1 -0
  36. package/.next/server/app/api/projects/route.js.nft.json +1 -0
  37. package/.next/server/app/api/projects/route_client-reference-manifest.js +1 -0
  38. package/.next/server/app/api/prs/[id]/merge/route.js +1 -0
  39. package/.next/server/app/api/prs/[id]/merge/route.js.nft.json +1 -0
  40. package/.next/server/app/api/prs/[id]/merge/route_client-reference-manifest.js +1 -0
  41. package/.next/server/app/api/runtime/terminal/route.js +1 -0
  42. package/.next/server/app/api/runtime/terminal/route.js.nft.json +1 -0
  43. package/.next/server/app/api/runtime/terminal/route_client-reference-manifest.js +1 -0
  44. package/.next/server/app/api/sessions/[id]/kill/route.js +1 -0
  45. package/.next/server/app/api/sessions/[id]/kill/route.js.nft.json +1 -0
  46. package/.next/server/app/api/sessions/[id]/kill/route_client-reference-manifest.js +1 -0
  47. package/.next/server/app/api/sessions/[id]/message/route.js +1 -0
  48. package/.next/server/app/api/sessions/[id]/message/route.js.nft.json +1 -0
  49. package/.next/server/app/api/sessions/[id]/message/route_client-reference-manifest.js +1 -0
  50. package/.next/server/app/api/sessions/[id]/remap/route.js +1 -0
  51. package/.next/server/app/api/sessions/[id]/remap/route.js.nft.json +1 -0
  52. package/.next/server/app/api/sessions/[id]/remap/route_client-reference-manifest.js +1 -0
  53. package/.next/server/app/api/sessions/[id]/restore/route.js +1 -0
  54. package/.next/server/app/api/sessions/[id]/restore/route.js.nft.json +1 -0
  55. package/.next/server/app/api/sessions/[id]/restore/route_client-reference-manifest.js +1 -0
  56. package/.next/server/app/api/sessions/[id]/route.js +1 -0
  57. package/.next/server/app/api/sessions/[id]/route.js.nft.json +1 -0
  58. package/.next/server/app/api/sessions/[id]/route_client-reference-manifest.js +1 -0
  59. package/.next/server/app/api/sessions/[id]/send/route.js +1 -0
  60. package/.next/server/app/api/sessions/[id]/send/route.js.nft.json +1 -0
  61. package/.next/server/app/api/sessions/[id]/send/route_client-reference-manifest.js +1 -0
  62. package/.next/server/app/api/sessions/route.js +1 -0
  63. package/.next/server/app/api/sessions/route.js.nft.json +1 -0
  64. package/.next/server/app/api/sessions/route_client-reference-manifest.js +1 -0
  65. package/.next/server/app/api/setup-labels/route.js +1 -0
  66. package/.next/server/app/api/setup-labels/route.js.nft.json +1 -0
  67. package/.next/server/app/api/setup-labels/route_client-reference-manifest.js +1 -0
  68. package/.next/server/app/api/spawn/route.js +1 -0
  69. package/.next/server/app/api/spawn/route.js.nft.json +1 -0
  70. package/.next/server/app/api/spawn/route_client-reference-manifest.js +1 -0
  71. package/.next/server/app/api/verify/route.js +1 -0
  72. package/.next/server/app/api/verify/route.js.nft.json +1 -0
  73. package/.next/server/app/api/verify/route_client-reference-manifest.js +1 -0
  74. package/.next/server/app/api/webhooks/[...slug]/route.js +1 -0
  75. package/.next/server/app/api/webhooks/[...slug]/route.js.nft.json +1 -0
  76. package/.next/server/app/api/webhooks/[...slug]/route_client-reference-manifest.js +1 -0
  77. package/.next/server/app/apple-icon/route.js +1 -0
  78. package/.next/server/app/apple-icon/route.js.nft.json +1 -0
  79. package/.next/server/app/apple-icon/route_client-reference-manifest.js +1 -0
  80. package/.next/server/app/apple-icon.body +0 -0
  81. package/.next/server/app/apple-icon.meta +1 -0
  82. package/.next/server/app/dev/terminal-test/page.js +15 -0
  83. package/.next/server/app/dev/terminal-test/page.js.nft.json +1 -0
  84. package/.next/server/app/dev/terminal-test/page_client-reference-manifest.js +1 -0
  85. package/.next/server/app/dev/terminal-test.html +1 -0
  86. package/.next/server/app/dev/terminal-test.meta +7 -0
  87. package/.next/server/app/dev/terminal-test.rsc +27 -0
  88. package/.next/server/app/icon/route.js +1 -0
  89. package/.next/server/app/icon/route.js.nft.json +1 -0
  90. package/.next/server/app/icon/route_client-reference-manifest.js +1 -0
  91. package/.next/server/app/icon-192/route.js +1 -0
  92. package/.next/server/app/icon-192/route.js.nft.json +1 -0
  93. package/.next/server/app/icon-192/route_client-reference-manifest.js +1 -0
  94. package/.next/server/app/icon-512/route.js +1 -0
  95. package/.next/server/app/icon-512/route.js.nft.json +1 -0
  96. package/.next/server/app/icon-512/route_client-reference-manifest.js +1 -0
  97. package/.next/server/app/icon.body +0 -0
  98. package/.next/server/app/icon.meta +1 -0
  99. package/.next/server/app/manifest.webmanifest/route.js +16 -0
  100. package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -0
  101. package/.next/server/app/manifest.webmanifest/route_client-reference-manifest.js +1 -0
  102. package/.next/server/app/manifest.webmanifest.body +1 -0
  103. package/.next/server/app/manifest.webmanifest.meta +1 -0
  104. package/.next/server/app/orchestrators/page.js +2 -0
  105. package/.next/server/app/orchestrators/page.js.nft.json +1 -0
  106. package/.next/server/app/orchestrators/page_client-reference-manifest.js +1 -0
  107. package/.next/server/app/page.js +2 -0
  108. package/.next/server/app/page.js.nft.json +1 -0
  109. package/.next/server/app/page_client-reference-manifest.js +1 -0
  110. package/.next/server/app/prs/page.js +2 -0
  111. package/.next/server/app/prs/page.js.nft.json +1 -0
  112. package/.next/server/app/prs/page_client-reference-manifest.js +1 -0
  113. package/.next/server/app/sessions/[id]/page.js +12 -0
  114. package/.next/server/app/sessions/[id]/page.js.nft.json +1 -0
  115. package/.next/server/app/sessions/[id]/page_client-reference-manifest.js +1 -0
  116. package/.next/server/app/test-direct/page.js +2 -0
  117. package/.next/server/app/test-direct/page.js.nft.json +1 -0
  118. package/.next/server/app/test-direct/page_client-reference-manifest.js +1 -0
  119. package/.next/server/app/test-direct.html +1 -0
  120. package/.next/server/app/test-direct.meta +7 -0
  121. package/.next/server/app/test-direct.rsc +27 -0
  122. package/.next/server/app-paths-manifest.json +33 -0
  123. package/.next/server/chunks/27.js +438 -0
  124. package/.next/server/chunks/377.js +1 -0
  125. package/.next/server/chunks/393.js +1 -0
  126. package/.next/server/chunks/627.js +391 -0
  127. package/.next/server/chunks/639.js +6 -0
  128. package/.next/server/chunks/693.js +1 -0
  129. package/.next/server/chunks/705.js +22 -0
  130. package/.next/server/chunks/787.js +29 -0
  131. package/.next/server/chunks/796.js +1 -0
  132. package/.next/server/chunks/934.js +1 -0
  133. package/.next/server/chunks/956.js +9 -0
  134. package/.next/server/functions-config-manifest.json +4 -0
  135. package/.next/server/interception-route-rewrite-manifest.js +1 -0
  136. package/.next/server/middleware-build-manifest.js +1 -0
  137. package/.next/server/middleware-manifest.json +6 -0
  138. package/.next/server/middleware-react-loadable-manifest.js +1 -0
  139. package/.next/server/next-font-manifest.js +1 -0
  140. package/.next/server/next-font-manifest.json +1 -0
  141. package/.next/server/pages/404.html +1 -0
  142. package/.next/server/pages/500.html +1 -0
  143. package/.next/server/pages/_app.js +1 -0
  144. package/.next/server/pages/_app.js.nft.json +1 -0
  145. package/.next/server/pages/_document.js +1 -0
  146. package/.next/server/pages/_document.js.nft.json +1 -0
  147. package/.next/server/pages/_error.js +19 -0
  148. package/.next/server/pages/_error.js.nft.json +1 -0
  149. package/.next/server/pages-manifest.json +6 -0
  150. package/.next/server/server-reference-manifest.js +1 -0
  151. package/.next/server/server-reference-manifest.json +1 -0
  152. package/.next/server/webpack-runtime.js +1 -0
  153. package/.next/static/3WYmn9ZvJ5NqH2dUKVdTc/_buildManifest.js +1 -0
  154. package/.next/static/3WYmn9ZvJ5NqH2dUKVdTc/_ssgManifest.js +1 -0
  155. package/.next/static/chunks/1250-e7cf6b069fbb03ed.js +1 -0
  156. package/.next/static/chunks/2205.498806f73783aa54.js +1 -0
  157. package/.next/static/chunks/3698-9c12c45b8184022c.js +1 -0
  158. package/.next/static/chunks/6381.1541d5695a727108.js +1 -0
  159. package/.next/static/chunks/7411.ecda44797fb514a0.js +1 -0
  160. package/.next/static/chunks/7505-2d2422d31862995f.js +1 -0
  161. package/.next/static/chunks/8597-1385f90ec1cebf47.js +1 -0
  162. package/.next/static/chunks/8762.f3d526855363db16.js +1 -0
  163. package/.next/static/chunks/9393.acf1934a190d793b.js +1 -0
  164. package/.next/static/chunks/a51c26f2-a21e680a5df6764e.js +1 -0
  165. package/.next/static/chunks/app/_not-found/page-2224bc1d3dce1b3e.js +1 -0
  166. package/.next/static/chunks/app/api/backlog/route-2224bc1d3dce1b3e.js +1 -0
  167. package/.next/static/chunks/app/api/events/route-2224bc1d3dce1b3e.js +1 -0
  168. package/.next/static/chunks/app/api/issues/route-2224bc1d3dce1b3e.js +1 -0
  169. package/.next/static/chunks/app/api/observability/route-2224bc1d3dce1b3e.js +1 -0
  170. package/.next/static/chunks/app/api/orchestrators/route-2224bc1d3dce1b3e.js +1 -0
  171. package/.next/static/chunks/app/api/projects/route-2224bc1d3dce1b3e.js +1 -0
  172. package/.next/static/chunks/app/api/prs/[id]/merge/route-2224bc1d3dce1b3e.js +1 -0
  173. package/.next/static/chunks/app/api/runtime/terminal/route-2224bc1d3dce1b3e.js +1 -0
  174. package/.next/static/chunks/app/api/sessions/[id]/kill/route-2224bc1d3dce1b3e.js +1 -0
  175. package/.next/static/chunks/app/api/sessions/[id]/message/route-2224bc1d3dce1b3e.js +1 -0
  176. package/.next/static/chunks/app/api/sessions/[id]/remap/route-2224bc1d3dce1b3e.js +1 -0
  177. package/.next/static/chunks/app/api/sessions/[id]/restore/route-2224bc1d3dce1b3e.js +1 -0
  178. package/.next/static/chunks/app/api/sessions/[id]/route-2224bc1d3dce1b3e.js +1 -0
  179. package/.next/static/chunks/app/api/sessions/[id]/send/route-2224bc1d3dce1b3e.js +1 -0
  180. package/.next/static/chunks/app/api/sessions/route-2224bc1d3dce1b3e.js +1 -0
  181. package/.next/static/chunks/app/api/setup-labels/route-2224bc1d3dce1b3e.js +1 -0
  182. package/.next/static/chunks/app/api/spawn/route-2224bc1d3dce1b3e.js +1 -0
  183. package/.next/static/chunks/app/api/verify/route-2224bc1d3dce1b3e.js +1 -0
  184. package/.next/static/chunks/app/api/webhooks/[...slug]/route-2224bc1d3dce1b3e.js +1 -0
  185. package/.next/static/chunks/app/apple-icon/route-2224bc1d3dce1b3e.js +1 -0
  186. package/.next/static/chunks/app/dev/terminal-test/page-ac0ce5b046fcad82.js +1 -0
  187. package/.next/static/chunks/app/error-4896c9d3b7681a80.js +1 -0
  188. package/.next/static/chunks/app/global-error-7b5c8ae45329c659.js +1 -0
  189. package/.next/static/chunks/app/icon/route-2224bc1d3dce1b3e.js +1 -0
  190. package/.next/static/chunks/app/icon-192/route-2224bc1d3dce1b3e.js +1 -0
  191. package/.next/static/chunks/app/icon-512/route-2224bc1d3dce1b3e.js +1 -0
  192. package/.next/static/chunks/app/layout-023f2083204e4f68.js +1 -0
  193. package/.next/static/chunks/app/loading-2224bc1d3dce1b3e.js +1 -0
  194. package/.next/static/chunks/app/manifest.webmanifest/route-2224bc1d3dce1b3e.js +1 -0
  195. package/.next/static/chunks/app/not-found-3772c2e09c29d80f.js +1 -0
  196. package/.next/static/chunks/app/orchestrators/page-df85236df674fc5a.js +1 -0
  197. package/.next/static/chunks/app/page-e97da633b7ef25f2.js +1 -0
  198. package/.next/static/chunks/app/prs/page-8cc0fce584d238c5.js +1 -0
  199. package/.next/static/chunks/app/sessions/[id]/error-90bc99b777fecabf.js +1 -0
  200. package/.next/static/chunks/app/sessions/[id]/loading-2224bc1d3dce1b3e.js +1 -0
  201. package/.next/static/chunks/app/sessions/[id]/not-found-3772c2e09c29d80f.js +1 -0
  202. package/.next/static/chunks/app/sessions/[id]/page-c9c95a8604786cff.js +1 -0
  203. package/.next/static/chunks/app/test-direct/page-16ceeb9f7664394a.js +1 -0
  204. package/.next/static/chunks/df4ed4d4.6a752eba3933e9a8.js +3 -0
  205. package/.next/static/chunks/framework-f8b8ba0f71d38056.js +1 -0
  206. package/.next/static/chunks/main-2bc85c765bf1fc49.js +1 -0
  207. package/.next/static/chunks/main-app-93fb36c3bd1a6739.js +1 -0
  208. package/.next/static/chunks/pages/_app-a0b975794f15bd68.js +1 -0
  209. package/.next/static/chunks/pages/_error-5f4e3b5eea57917d.js +1 -0
  210. package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
  211. package/.next/static/chunks/webpack-e12ceebeb7a1cc7e.js +1 -0
  212. package/.next/static/css/46a7e8e962075e54.css +1 -0
  213. package/.next/static/css/6ef2fa08dd043252.css +1 -0
  214. package/.next/static/css/908f93fdd7ffba42.css +1 -0
  215. package/.next/static/media/4cf2300e9c8272f7-s.p.woff2 +0 -0
  216. package/.next/static/media/558ca1a6aa3cb55e-s.p.woff2 +0 -0
  217. package/.next/static/media/64d784ea54a4acde-s.woff2 +0 -0
  218. package/.next/static/media/6d831b18ae5b01dc-s.woff2 +0 -0
  219. package/.next/static/media/8d697b304b401681-s.woff2 +0 -0
  220. package/.next/static/media/ac0e76ddaeeb7981-s.woff2 +0 -0
  221. package/.next/static/media/ba015fad6dcf6784-s.woff2 +0 -0
  222. package/.next/static/media/edc640959b0c7826-s.woff2 +0 -0
  223. package/.next/static/media/ff71da380fbe67dd-s.woff2 +0 -0
  224. package/dist-server/direct-terminal-ws.js +297 -0
  225. package/dist-server/start-all.js +113 -0
  226. package/dist-server/terminal-observability.js +16 -0
  227. package/dist-server/terminal-websocket.js +375 -0
  228. package/dist-server/tmux-utils.js +89 -0
  229. package/next.config.js +33 -0
  230. package/package.json +71 -0
@@ -0,0 +1,297 @@
1
+ /**
2
+ * Direct WebSocket terminal server using node-pty.
3
+ * Connects browser xterm.js directly to tmux sessions via WebSocket.
4
+ *
5
+ * This bypasses ttyd and gives us control over terminal initialization,
6
+ * allowing us to implement the XDA (Extended Device Attributes) handler
7
+ * that tmux requires for clipboard support.
8
+ */
9
+ import { createServer } from "node:http";
10
+ import { spawn } from "node:child_process";
11
+ import { WebSocketServer, WebSocket } from "ws";
12
+ import { homedir, userInfo } from "node:os";
13
+ import { createCorrelationId } from "@aoagents/ao-core";
14
+ let ptySpawn;
15
+ /* eslint-enable @typescript-eslint/consistent-type-imports */
16
+ try {
17
+ const nodePty = await import("node-pty");
18
+ ptySpawn = nodePty.spawn;
19
+ }
20
+ catch {
21
+ console.warn("[DirectTerminal] node-pty not available — direct terminal will be disabled.");
22
+ console.warn("[DirectTerminal] Install it with: npm install node-pty");
23
+ }
24
+ import { findTmux, resolveTmuxSession, validateSessionId } from "./tmux-utils.js";
25
+ import { createObserverContext, inferProjectId } from "./terminal-observability.js";
26
+ /**
27
+ * Create the direct terminal WebSocket server.
28
+ * Separated from listen() so tests can control lifecycle.
29
+ */
30
+ export function createDirectTerminalServer(tmuxPath) {
31
+ const TMUX = tmuxPath ?? findTmux();
32
+ const activeSessions = new Map();
33
+ const { config, observer } = createObserverContext("terminal-direct-websocket");
34
+ const metrics = {
35
+ activeConnections: 0,
36
+ totalConnections: 0,
37
+ totalDisconnects: 0,
38
+ totalErrors: 0,
39
+ lastConnectedAt: null,
40
+ lastDisconnectedAt: null,
41
+ lastErrorAt: null,
42
+ lastDisconnectReason: null,
43
+ lastErrorReason: null,
44
+ };
45
+ const server = createServer((req, res) => {
46
+ if (req.url === "/health") {
47
+ res.writeHead(200, { "Content-Type": "application/json" });
48
+ res.end(JSON.stringify({
49
+ active: activeSessions.size,
50
+ sessions: Array.from(activeSessions.keys()),
51
+ metrics,
52
+ }));
53
+ return;
54
+ }
55
+ res.writeHead(404);
56
+ res.end("Not found");
57
+ });
58
+ const wss = new WebSocketServer({
59
+ server,
60
+ path: "/ws",
61
+ });
62
+ const recordWebsocketMetric = (input) => {
63
+ if (!observer) {
64
+ return;
65
+ }
66
+ const correlationId = createCorrelationId("ws");
67
+ observer.recordOperation({
68
+ metric: input.metric,
69
+ operation: `terminal.websocket.${input.metric}`,
70
+ outcome: input.outcome,
71
+ correlationId,
72
+ projectId: input.sessionId ? inferProjectId(config, input.sessionId) : undefined,
73
+ sessionId: input.sessionId,
74
+ reason: input.reason,
75
+ data: input.data,
76
+ level: input.outcome === "failure" ? "error" : "info",
77
+ });
78
+ };
79
+ wss.on("connection", (ws, req) => {
80
+ if (!ptySpawn) {
81
+ ws.close(1011, "Direct terminal unavailable — node-pty not installed");
82
+ return;
83
+ }
84
+ const url = new URL(req.url ?? "/", "ws://localhost");
85
+ const sessionId = url.searchParams.get("session");
86
+ if (!sessionId) {
87
+ console.error("[DirectTerminal] Missing session parameter");
88
+ recordWebsocketMetric({
89
+ metric: "websocket_error",
90
+ outcome: "failure",
91
+ reason: "Missing session parameter",
92
+ });
93
+ ws.close(1008, "Missing session parameter");
94
+ return;
95
+ }
96
+ // Validate session ID format
97
+ if (!validateSessionId(sessionId)) {
98
+ console.error("[DirectTerminal] Invalid session ID:", sessionId);
99
+ recordWebsocketMetric({
100
+ metric: "websocket_error",
101
+ outcome: "failure",
102
+ sessionId,
103
+ reason: "Invalid session ID",
104
+ });
105
+ ws.close(1008, "Invalid session ID");
106
+ return;
107
+ }
108
+ // Resolve tmux session name: try exact match first, then suffix match
109
+ // (hash-prefixed sessions like "8474d6f29887-ao-15" are accessed by user-facing ID "ao-15")
110
+ const tmuxSessionId = resolveTmuxSession(sessionId, TMUX);
111
+ if (!tmuxSessionId) {
112
+ console.error("[DirectTerminal] tmux session not found:", sessionId);
113
+ recordWebsocketMetric({
114
+ metric: "websocket_error",
115
+ outcome: "failure",
116
+ sessionId,
117
+ reason: "Session not found",
118
+ });
119
+ ws.close(1008, "Session not found");
120
+ return;
121
+ }
122
+ console.log(`[DirectTerminal] New connection for session: ${tmuxSessionId}`);
123
+ // Enable mouse mode for scrollback support
124
+ const mouseProc = spawn(TMUX, ["set-option", "-t", tmuxSessionId, "mouse", "on"]);
125
+ mouseProc.on("error", (err) => {
126
+ console.error(`[DirectTerminal] Failed to set mouse mode for ${tmuxSessionId}:`, err.message);
127
+ });
128
+ // Hide the green status bar for cleaner appearance
129
+ const statusProc = spawn(TMUX, ["set-option", "-t", tmuxSessionId, "status", "off"]);
130
+ statusProc.on("error", (err) => {
131
+ console.error(`[DirectTerminal] Failed to hide status bar for ${tmuxSessionId}:`, err.message);
132
+ });
133
+ // Build complete environment - node-pty requires proper env setup
134
+ const homeDir = process.env.HOME || homedir();
135
+ const currentUser = process.env.USER || userInfo().username;
136
+ const env = {
137
+ HOME: homeDir,
138
+ SHELL: process.env.SHELL || "/bin/bash",
139
+ USER: currentUser,
140
+ PATH: process.env.PATH || "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin",
141
+ TERM: "xterm-256color",
142
+ LANG: process.env.LANG || "en_US.UTF-8",
143
+ TMPDIR: process.env.TMPDIR || "/tmp",
144
+ };
145
+ let pty;
146
+ try {
147
+ console.log(`[DirectTerminal] Spawning PTY: tmux attach-session -t ${tmuxSessionId}`);
148
+ pty = ptySpawn(TMUX, ["attach-session", "-t", tmuxSessionId], {
149
+ name: "xterm-256color",
150
+ cols: 80,
151
+ rows: 24,
152
+ cwd: homeDir,
153
+ env,
154
+ });
155
+ console.log(`[DirectTerminal] PTY spawned successfully`);
156
+ }
157
+ catch (err) {
158
+ console.error(`[DirectTerminal] Failed to spawn PTY:`, err);
159
+ recordWebsocketMetric({
160
+ metric: "websocket_error",
161
+ outcome: "failure",
162
+ sessionId,
163
+ reason: err instanceof Error ? err.message : String(err),
164
+ });
165
+ ws.close(1011, `Failed to spawn terminal: ${err instanceof Error ? err.message : String(err)}`);
166
+ return;
167
+ }
168
+ const session = { sessionId, pty, ws };
169
+ activeSessions.set(sessionId, session);
170
+ metrics.totalConnections += 1;
171
+ metrics.activeConnections = activeSessions.size;
172
+ metrics.lastConnectedAt = new Date().toISOString();
173
+ recordWebsocketMetric({
174
+ metric: "websocket_connect",
175
+ outcome: "success",
176
+ sessionId,
177
+ data: { activeConnections: metrics.activeConnections },
178
+ });
179
+ let disconnectRecorded = false;
180
+ const recordDisconnect = (outcome, reason) => {
181
+ if (disconnectRecorded)
182
+ return;
183
+ disconnectRecorded = true;
184
+ const activeConnections = activeSessions.size;
185
+ metrics.activeConnections = activeConnections;
186
+ metrics.totalDisconnects += 1;
187
+ metrics.lastDisconnectedAt = new Date().toISOString();
188
+ metrics.lastDisconnectReason = reason;
189
+ recordWebsocketMetric({
190
+ metric: "websocket_disconnect",
191
+ outcome,
192
+ sessionId,
193
+ reason,
194
+ data: { activeConnections },
195
+ });
196
+ };
197
+ // PTY -> WebSocket
198
+ pty.onData((data) => {
199
+ if (ws.readyState === WebSocket.OPEN) {
200
+ ws.send(data);
201
+ }
202
+ });
203
+ // PTY exit
204
+ pty.onExit(({ exitCode }) => {
205
+ console.log(`[DirectTerminal] PTY exited for ${sessionId} with code ${exitCode}`);
206
+ // Guard against stale exits: only delete if this pty is still the active one.
207
+ // A new connection may have already replaced this session entry.
208
+ if (activeSessions.get(sessionId)?.pty === pty) {
209
+ activeSessions.delete(sessionId);
210
+ }
211
+ recordDisconnect(exitCode === 0 ? "success" : "failure", `pty_exit:${exitCode}`);
212
+ if (ws.readyState === WebSocket.OPEN) {
213
+ ws.close(1000, "Terminal session ended");
214
+ }
215
+ });
216
+ // WebSocket -> PTY
217
+ ws.on("message", (data) => {
218
+ const message = data.toString("utf8");
219
+ // Handle resize messages (sent by xterm.js FitAddon)
220
+ if (message.startsWith("{")) {
221
+ try {
222
+ const parsed = JSON.parse(message);
223
+ if (parsed.type === "resize" && parsed.cols && parsed.rows) {
224
+ pty.resize(parsed.cols, parsed.rows);
225
+ return;
226
+ }
227
+ }
228
+ catch {
229
+ // Not JSON, treat as terminal input
230
+ }
231
+ }
232
+ // Normal terminal input
233
+ pty.write(message);
234
+ });
235
+ // WebSocket close
236
+ ws.on("close", () => {
237
+ console.log(`[DirectTerminal] WebSocket closed for ${sessionId}`);
238
+ // Guard against stale closes replacing a newer session's entry
239
+ if (activeSessions.get(sessionId)?.pty === pty) {
240
+ activeSessions.delete(sessionId);
241
+ }
242
+ recordDisconnect("success", "ws_close");
243
+ pty.kill();
244
+ });
245
+ // WebSocket error
246
+ ws.on("error", (err) => {
247
+ console.error(`[DirectTerminal] WebSocket error for ${sessionId}:`, err.message);
248
+ // Guard against stale error handlers replacing a newer session's entry
249
+ if (activeSessions.get(sessionId)?.pty === pty) {
250
+ activeSessions.delete(sessionId);
251
+ }
252
+ recordDisconnect("failure", `ws_error:${err.message}`);
253
+ metrics.totalErrors += 1;
254
+ metrics.lastErrorAt = new Date().toISOString();
255
+ metrics.lastErrorReason = err.message;
256
+ recordWebsocketMetric({
257
+ metric: "websocket_error",
258
+ outcome: "failure",
259
+ sessionId,
260
+ reason: err.message,
261
+ });
262
+ pty.kill();
263
+ });
264
+ });
265
+ function shutdown() {
266
+ for (const [, session] of activeSessions) {
267
+ session.pty.kill();
268
+ session.ws.close(1001, "Server shutting down");
269
+ }
270
+ server.close();
271
+ }
272
+ return { server, wss, activeSessions, shutdown };
273
+ }
274
+ // --- Run as standalone script ---
275
+ // Only start the server when executed directly (not imported by tests)
276
+ const isMainModule = process.argv[1]?.endsWith("direct-terminal-ws.ts") ||
277
+ process.argv[1]?.endsWith("direct-terminal-ws.js");
278
+ if (isMainModule) {
279
+ const TMUX = findTmux();
280
+ console.log(`[DirectTerminal] Using tmux: ${TMUX}`);
281
+ const { server, shutdown } = createDirectTerminalServer(TMUX);
282
+ const PORT = parseInt(process.env.DIRECT_TERMINAL_PORT ?? "14801", 10);
283
+ server.listen(PORT, () => {
284
+ console.log(`[DirectTerminal] WebSocket server listening on port ${PORT}`);
285
+ });
286
+ function handleShutdown(signal) {
287
+ console.log(`[DirectTerminal] Received ${signal}, shutting down...`);
288
+ shutdown();
289
+ const forceExitTimer = setTimeout(() => {
290
+ console.error("[DirectTerminal] Forced shutdown after timeout");
291
+ process.exit(1);
292
+ }, 5000);
293
+ forceExitTimer.unref();
294
+ }
295
+ process.on("SIGINT", () => handleShutdown("SIGINT"));
296
+ process.on("SIGTERM", () => handleShutdown("SIGTERM"));
297
+ }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Production entry point — starts Next.js + terminal servers.
3
+ * Used by `ao start` when running from an npm install (no monorepo).
4
+ * Replaces the dev-only `concurrently` setup.
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ import { resolve, dirname } from "node:path";
8
+ import { existsSync } from "node:fs";
9
+ import { fileURLToPath } from "node:url";
10
+ import { createRequire } from "node:module";
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = dirname(__filename);
13
+ // Resolve paths relative to the package root (one level up from dist-server/)
14
+ const pkgRoot = resolve(__dirname, "..");
15
+ const children = [];
16
+ function log(label, msg) {
17
+ process.stdout.write(`[${label}] ${msg}\n`);
18
+ }
19
+ function spawnProcess(label, command, args, opts) {
20
+ let restarts = 0;
21
+ const maxRestarts = opts?.maxRestarts ?? 3;
22
+ let slotIndex = -1;
23
+ function launch() {
24
+ const child = spawn(command, args, {
25
+ cwd: pkgRoot,
26
+ stdio: ["ignore", "pipe", "pipe"],
27
+ env: process.env,
28
+ });
29
+ child.stdout?.on("data", (data) => {
30
+ for (const line of data.toString().split("\n").filter(Boolean)) {
31
+ log(label, line);
32
+ }
33
+ });
34
+ child.stderr?.on("data", (data) => {
35
+ for (const line of data.toString().split("\n").filter(Boolean)) {
36
+ log(label, line);
37
+ }
38
+ });
39
+ child.on("exit", (code) => {
40
+ log(label, `exited with code ${code}`);
41
+ if (!shuttingDown && opts?.restart && code !== 0 && restarts < maxRestarts) {
42
+ restarts++;
43
+ log(label, `restarting (attempt ${restarts}/${maxRestarts})`);
44
+ const replacement = launch();
45
+ // Replace in-place — slot was assigned on first push
46
+ children[slotIndex] = replacement;
47
+ }
48
+ });
49
+ // Only push on first launch; restarts replace the existing slot
50
+ if (slotIndex === -1) {
51
+ slotIndex = children.length;
52
+ children.push(child);
53
+ }
54
+ return child;
55
+ }
56
+ return launch();
57
+ }
58
+ /**
59
+ * Resolve the `next` CLI binary path.
60
+ * Tries the local .bin shim first (fast), then falls back to require.resolve (hoisted deps).
61
+ */
62
+ function resolveNextBin() {
63
+ const localBin = resolve(pkgRoot, "node_modules", ".bin", "next");
64
+ if (existsSync(localBin))
65
+ return localBin;
66
+ // Hoisted node_modules — resolve the actual next CLI entry
67
+ const require = createRequire(resolve(pkgRoot, "package.json"));
68
+ try {
69
+ const nextPkg = require.resolve("next/package.json");
70
+ return resolve(dirname(nextPkg), "dist", "bin", "next");
71
+ }
72
+ catch {
73
+ // Last resort — rely on PATH
74
+ return "next";
75
+ }
76
+ }
77
+ // Start Next.js production server
78
+ const port = process.env["PORT"] || "3000";
79
+ spawnProcess("next", resolveNextBin(), ["start", "-p", port]);
80
+ // Start terminal WebSocket server (auto-restart on crash)
81
+ spawnProcess("terminal", "node", [resolve(__dirname, "terminal-websocket.js")], { restart: true });
82
+ // Start direct terminal WebSocket server (auto-restart on crash)
83
+ spawnProcess("direct-terminal", "node", [resolve(__dirname, "direct-terminal-ws.js")], { restart: true });
84
+ // Graceful shutdown — send SIGTERM to children and wait for them to exit
85
+ let shuttingDown = false;
86
+ function cleanup() {
87
+ if (shuttingDown)
88
+ return;
89
+ shuttingDown = true;
90
+ let alive = children.length;
91
+ if (alive === 0) {
92
+ process.exit(0);
93
+ return;
94
+ }
95
+ // Force exit after 5s if children don't exit cleanly
96
+ const forceTimer = setTimeout(() => {
97
+ log("start-all", "Children did not exit in time, forcing shutdown");
98
+ process.exit(1);
99
+ }, 5000);
100
+ forceTimer.unref();
101
+ for (const child of children) {
102
+ child.on("exit", () => {
103
+ alive--;
104
+ if (alive <= 0) {
105
+ clearTimeout(forceTimer);
106
+ process.exit(0);
107
+ }
108
+ });
109
+ child.kill("SIGTERM");
110
+ }
111
+ }
112
+ process.on("SIGINT", cleanup);
113
+ process.on("SIGTERM", cleanup);
@@ -0,0 +1,16 @@
1
+ import { createProjectObserver, loadConfig, resolveProjectIdForSessionId, } from "@aoagents/ao-core";
2
+ export function createObserverContext(surface) {
3
+ try {
4
+ const config = loadConfig();
5
+ return {
6
+ config,
7
+ observer: createProjectObserver(config, surface),
8
+ };
9
+ }
10
+ catch {
11
+ return { config: undefined, observer: undefined };
12
+ }
13
+ }
14
+ export function inferProjectId(config, sessionId) {
15
+ return config ? resolveProjectIdForSessionId(config, sessionId) : undefined;
16
+ }