gigaclaw 1.4.0

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 (249) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +237 -0
  3. package/api/CLAUDE.md +19 -0
  4. package/api/index.js +265 -0
  5. package/bin/cli.js +823 -0
  6. package/bin/local.sh +85 -0
  7. package/bin/postinstall.js +63 -0
  8. package/config/index.js +26 -0
  9. package/config/instrumentation.js +62 -0
  10. package/drizzle/0000_initial.sql +52 -0
  11. package/drizzle/0001_nostalgic_sersi.sql +11 -0
  12. package/drizzle/0002_black_daimon_hellstrom.sql +19 -0
  13. package/drizzle/0003_rename_code_workspaces.sql +5 -0
  14. package/drizzle/meta/0000_snapshot.json +321 -0
  15. package/drizzle/meta/0001_snapshot.json +390 -0
  16. package/drizzle/meta/0002_snapshot.json +411 -0
  17. package/drizzle/meta/0003_snapshot.json +419 -0
  18. package/drizzle/meta/_journal.json +34 -0
  19. package/lib/actions.js +44 -0
  20. package/lib/ai/agent.js +86 -0
  21. package/lib/ai/index.js +342 -0
  22. package/lib/ai/model.js +180 -0
  23. package/lib/ai/tools.js +269 -0
  24. package/lib/ai/web-search.js +42 -0
  25. package/lib/auth/actions.js +28 -0
  26. package/lib/auth/config.js +27 -0
  27. package/lib/auth/edge-config.js +27 -0
  28. package/lib/auth/index.js +27 -0
  29. package/lib/auth/middleware.js +62 -0
  30. package/lib/channels/base.js +56 -0
  31. package/lib/channels/index.js +15 -0
  32. package/lib/channels/telegram.js +148 -0
  33. package/lib/chat/actions.js +579 -0
  34. package/lib/chat/api.js +140 -0
  35. package/lib/chat/components/app-sidebar.js +213 -0
  36. package/lib/chat/components/app-sidebar.jsx +279 -0
  37. package/lib/chat/components/chat-header.js +192 -0
  38. package/lib/chat/components/chat-header.jsx +223 -0
  39. package/lib/chat/components/chat-input.js +236 -0
  40. package/lib/chat/components/chat-input.jsx +249 -0
  41. package/lib/chat/components/chat-nav-context.js +11 -0
  42. package/lib/chat/components/chat-nav-context.jsx +11 -0
  43. package/lib/chat/components/chat-page.js +99 -0
  44. package/lib/chat/components/chat-page.jsx +121 -0
  45. package/lib/chat/components/chat.js +153 -0
  46. package/lib/chat/components/chat.jsx +199 -0
  47. package/lib/chat/components/chats-page.js +367 -0
  48. package/lib/chat/components/chats-page.jsx +394 -0
  49. package/lib/chat/components/code-mode-toggle.js +132 -0
  50. package/lib/chat/components/code-mode-toggle.jsx +163 -0
  51. package/lib/chat/components/crons-page.js +172 -0
  52. package/lib/chat/components/crons-page.jsx +244 -0
  53. package/lib/chat/components/greeting.js +11 -0
  54. package/lib/chat/components/greeting.jsx +16 -0
  55. package/lib/chat/components/icons.js +805 -0
  56. package/lib/chat/components/icons.jsx +751 -0
  57. package/lib/chat/components/index.js +20 -0
  58. package/lib/chat/components/message.js +363 -0
  59. package/lib/chat/components/message.jsx +422 -0
  60. package/lib/chat/components/messages.js +65 -0
  61. package/lib/chat/components/messages.jsx +74 -0
  62. package/lib/chat/components/notifications-page.js +56 -0
  63. package/lib/chat/components/notifications-page.jsx +87 -0
  64. package/lib/chat/components/page-layout.js +21 -0
  65. package/lib/chat/components/page-layout.jsx +28 -0
  66. package/lib/chat/components/pull-requests-page.js +103 -0
  67. package/lib/chat/components/pull-requests-page.jsx +113 -0
  68. package/lib/chat/components/settings-layout.js +39 -0
  69. package/lib/chat/components/settings-layout.jsx +53 -0
  70. package/lib/chat/components/settings-secrets-page.js +216 -0
  71. package/lib/chat/components/settings-secrets-page.jsx +264 -0
  72. package/lib/chat/components/sidebar-history-item.js +138 -0
  73. package/lib/chat/components/sidebar-history-item.jsx +119 -0
  74. package/lib/chat/components/sidebar-history.js +167 -0
  75. package/lib/chat/components/sidebar-history.jsx +220 -0
  76. package/lib/chat/components/sidebar-user-nav.js +61 -0
  77. package/lib/chat/components/sidebar-user-nav.jsx +77 -0
  78. package/lib/chat/components/swarm-page.js +157 -0
  79. package/lib/chat/components/swarm-page.jsx +210 -0
  80. package/lib/chat/components/tool-call.js +89 -0
  81. package/lib/chat/components/tool-call.jsx +107 -0
  82. package/lib/chat/components/triggers-page.js +153 -0
  83. package/lib/chat/components/triggers-page.jsx +221 -0
  84. package/lib/chat/components/ui/combobox.js +98 -0
  85. package/lib/chat/components/ui/combobox.jsx +114 -0
  86. package/lib/chat/components/ui/confirm-dialog.js +53 -0
  87. package/lib/chat/components/ui/confirm-dialog.jsx +57 -0
  88. package/lib/chat/components/ui/dropdown-menu.js +194 -0
  89. package/lib/chat/components/ui/dropdown-menu.jsx +215 -0
  90. package/lib/chat/components/ui/rename-dialog.js +78 -0
  91. package/lib/chat/components/ui/rename-dialog.jsx +74 -0
  92. package/lib/chat/components/ui/scroll-area.js +13 -0
  93. package/lib/chat/components/ui/scroll-area.jsx +17 -0
  94. package/lib/chat/components/ui/separator.js +21 -0
  95. package/lib/chat/components/ui/separator.jsx +18 -0
  96. package/lib/chat/components/ui/sheet.js +75 -0
  97. package/lib/chat/components/ui/sheet.jsx +95 -0
  98. package/lib/chat/components/ui/sidebar.js +228 -0
  99. package/lib/chat/components/ui/sidebar.jsx +246 -0
  100. package/lib/chat/components/ui/tooltip.js +56 -0
  101. package/lib/chat/components/ui/tooltip.jsx +66 -0
  102. package/lib/chat/components/upgrade-dialog.js +151 -0
  103. package/lib/chat/components/upgrade-dialog.jsx +170 -0
  104. package/lib/chat/utils.js +11 -0
  105. package/lib/code/actions.js +153 -0
  106. package/lib/code/code-page.js +22 -0
  107. package/lib/code/code-page.jsx +25 -0
  108. package/lib/code/index.js +1 -0
  109. package/lib/code/terminal-view.js +201 -0
  110. package/lib/code/terminal-view.jsx +224 -0
  111. package/lib/code/ws-proxy.js +80 -0
  112. package/lib/cron.js +246 -0
  113. package/lib/db/api-keys.js +163 -0
  114. package/lib/db/chats.js +168 -0
  115. package/lib/db/code-workspaces.js +110 -0
  116. package/lib/db/index.js +52 -0
  117. package/lib/db/notifications.js +99 -0
  118. package/lib/db/schema.js +66 -0
  119. package/lib/db/update-check.js +96 -0
  120. package/lib/db/users.js +89 -0
  121. package/lib/paths.js +42 -0
  122. package/lib/tools/create-job.js +97 -0
  123. package/lib/tools/docker.js +146 -0
  124. package/lib/tools/github.js +271 -0
  125. package/lib/tools/openai.js +35 -0
  126. package/lib/tools/telegram.js +292 -0
  127. package/lib/triggers.js +104 -0
  128. package/lib/utils/render-md.js +111 -0
  129. package/package.json +118 -0
  130. package/setup/lib/auth.mjs +81 -0
  131. package/setup/lib/env.mjs +21 -0
  132. package/setup/lib/fs-utils.mjs +20 -0
  133. package/setup/lib/github.mjs +149 -0
  134. package/setup/lib/prerequisites.mjs +155 -0
  135. package/setup/lib/prompts.mjs +267 -0
  136. package/setup/lib/providers.mjs +105 -0
  137. package/setup/lib/sync.mjs +125 -0
  138. package/setup/lib/targets.mjs +45 -0
  139. package/setup/lib/telegram-verify.mjs +63 -0
  140. package/setup/lib/telegram.mjs +76 -0
  141. package/setup/setup-cloud.mjs +833 -0
  142. package/setup/setup-local.mjs +377 -0
  143. package/setup/setup-telegram.mjs +265 -0
  144. package/setup/setup.mjs +87 -0
  145. package/templates/.dockerignore +5 -0
  146. package/templates/.env.example +104 -0
  147. package/templates/.github/workflows/auto-merge.yml +117 -0
  148. package/templates/.github/workflows/notify-job-failed.yml +64 -0
  149. package/templates/.github/workflows/notify-pr-complete.yml +119 -0
  150. package/templates/.github/workflows/rebuild-event-handler.yml +121 -0
  151. package/templates/.github/workflows/run-job.yml +89 -0
  152. package/templates/.github/workflows/upgrade-event-handler.yml +62 -0
  153. package/templates/.gitignore.template +45 -0
  154. package/templates/.pi/extensions/env-sanitizer/index.ts +48 -0
  155. package/templates/.pi/extensions/env-sanitizer/package.json +5 -0
  156. package/templates/CLAUDE.md +29 -0
  157. package/templates/CLAUDE.md.template +308 -0
  158. package/templates/app/api/[...gigaclaw]/route.js +1 -0
  159. package/templates/app/api/auth/[...nextauth]/route.js +1 -0
  160. package/templates/app/chat/[chatId]/page.js +9 -0
  161. package/templates/app/chats/page.js +7 -0
  162. package/templates/app/code/[codeWorkspaceId]/page.js +9 -0
  163. package/templates/app/components/ascii-logo.jsx +12 -0
  164. package/templates/app/components/login-form.jsx +92 -0
  165. package/templates/app/components/setup-form.jsx +82 -0
  166. package/templates/app/components/theme-provider.jsx +11 -0
  167. package/templates/app/components/theme-toggle.jsx +38 -0
  168. package/templates/app/components/ui/button.jsx +21 -0
  169. package/templates/app/components/ui/card.jsx +23 -0
  170. package/templates/app/components/ui/input.jsx +10 -0
  171. package/templates/app/components/ui/label.jsx +10 -0
  172. package/templates/app/crons/page.js +5 -0
  173. package/templates/app/globals.css +90 -0
  174. package/templates/app/layout.js +33 -0
  175. package/templates/app/login/page.js +15 -0
  176. package/templates/app/notifications/page.js +7 -0
  177. package/templates/app/page.js +7 -0
  178. package/templates/app/pull-requests/page.js +7 -0
  179. package/templates/app/settings/crons/page.js +5 -0
  180. package/templates/app/settings/layout.js +7 -0
  181. package/templates/app/settings/page.js +5 -0
  182. package/templates/app/settings/secrets/page.js +5 -0
  183. package/templates/app/settings/triggers/page.js +5 -0
  184. package/templates/app/stream/chat/route.js +1 -0
  185. package/templates/app/swarm/page.js +7 -0
  186. package/templates/app/triggers/page.js +5 -0
  187. package/templates/config/CODE_PLANNING.md +14 -0
  188. package/templates/config/CRONS.json +56 -0
  189. package/templates/config/HEARTBEAT.md +3 -0
  190. package/templates/config/JOB_AGENT.md +30 -0
  191. package/templates/config/JOB_PLANNING.md +240 -0
  192. package/templates/config/JOB_SUMMARY.md +130 -0
  193. package/templates/config/SKILL_BUILDING_GUIDE.md +96 -0
  194. package/templates/config/SOUL.md +48 -0
  195. package/templates/config/TRIGGERS.json +58 -0
  196. package/templates/config/WEB_SEARCH_AVAILABLE.md +5 -0
  197. package/templates/config/WEB_SEARCH_UNAVAILABLE.md +3 -0
  198. package/templates/docker/claude-code-job/Dockerfile +34 -0
  199. package/templates/docker/claude-code-job/entrypoint.sh +149 -0
  200. package/templates/docker/claude-code-workspace/.tmux.conf +5 -0
  201. package/templates/docker/claude-code-workspace/Dockerfile +61 -0
  202. package/templates/docker/claude-code-workspace/entrypoint.sh +51 -0
  203. package/templates/docker/event-handler/Dockerfile +20 -0
  204. package/templates/docker/event-handler/ecosystem.config.cjs +7 -0
  205. package/templates/docker/pi-coding-agent-job/Dockerfile +51 -0
  206. package/templates/docker/pi-coding-agent-job/entrypoint.sh +164 -0
  207. package/templates/docker-compose.local.yml +78 -0
  208. package/templates/docker-compose.yml +64 -0
  209. package/templates/instrumentation.js +6 -0
  210. package/templates/middleware.js +23 -0
  211. package/templates/next.config.mjs +3 -0
  212. package/templates/postcss.config.mjs +5 -0
  213. package/templates/public/favicon.ico +0 -0
  214. package/templates/server.js +25 -0
  215. package/templates/skills/LICENSE +21 -0
  216. package/templates/skills/README.md +119 -0
  217. package/templates/skills/brave-search/SKILL.md +79 -0
  218. package/templates/skills/brave-search/content.js +86 -0
  219. package/templates/skills/brave-search/package-lock.json +621 -0
  220. package/templates/skills/brave-search/package.json +14 -0
  221. package/templates/skills/brave-search/search.js +199 -0
  222. package/templates/skills/browser-tools/SKILL.md +196 -0
  223. package/templates/skills/browser-tools/browser-content.js +103 -0
  224. package/templates/skills/browser-tools/browser-cookies.js +35 -0
  225. package/templates/skills/browser-tools/browser-eval.js +53 -0
  226. package/templates/skills/browser-tools/browser-hn-scraper.js +108 -0
  227. package/templates/skills/browser-tools/browser-nav.js +44 -0
  228. package/templates/skills/browser-tools/browser-pick.js +162 -0
  229. package/templates/skills/browser-tools/browser-screenshot.js +34 -0
  230. package/templates/skills/browser-tools/browser-start.js +87 -0
  231. package/templates/skills/browser-tools/package-lock.json +2556 -0
  232. package/templates/skills/browser-tools/package.json +19 -0
  233. package/templates/skills/google-docs/SKILL.md +23 -0
  234. package/templates/skills/google-docs/create.sh +69 -0
  235. package/templates/skills/google-drive/SKILL.md +47 -0
  236. package/templates/skills/google-drive/delete.sh +47 -0
  237. package/templates/skills/google-drive/download.sh +50 -0
  238. package/templates/skills/google-drive/list.sh +41 -0
  239. package/templates/skills/google-drive/upload.sh +76 -0
  240. package/templates/skills/kie-ai/SKILL.md +38 -0
  241. package/templates/skills/kie-ai/generate-image.sh +77 -0
  242. package/templates/skills/kie-ai/generate-video.sh +69 -0
  243. package/templates/skills/llm-secrets/SKILL.md +34 -0
  244. package/templates/skills/llm-secrets/llm-secrets.js +33 -0
  245. package/templates/skills/modify-self/SKILL.md +12 -0
  246. package/templates/skills/youtube-transcript/SKILL.md +41 -0
  247. package/templates/skills/youtube-transcript/package-lock.json +24 -0
  248. package/templates/skills/youtube-transcript/package.json +8 -0
  249. package/templates/skills/youtube-transcript/transcript.js +84 -0
@@ -0,0 +1,201 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useCallback, useState } from "react";
4
+ import { Terminal } from "@xterm/xterm";
5
+ import { FitAddon } from "@xterm/addon-fit";
6
+ import { SearchAddon } from "@xterm/addon-search";
7
+ import { WebLinksAddon } from "@xterm/addon-web-links";
8
+ import { SerializeAddon } from "@xterm/addon-serialize";
9
+ import { ensureCodeWorkspaceContainer } from "./actions.js";
10
+ import "@xterm/xterm/css/xterm.css";
11
+ const STATUS = { connected: "#22c55e", connecting: "#eab308", disconnected: "#ef4444" };
12
+ const RECONNECT_INTERVAL = 3e3;
13
+ function TerminalView({ codeWorkspaceId }) {
14
+ const containerRef = useRef(null);
15
+ const termRef = useRef(null);
16
+ const fitAddonRef = useRef(null);
17
+ const wsRef = useRef(null);
18
+ const retryTimer = useRef(null);
19
+ const statusRef = useRef(null);
20
+ const [connected, setConnected] = useState(false);
21
+ const setStatus = useCallback((color) => {
22
+ if (statusRef.current) statusRef.current.style.backgroundColor = color;
23
+ setConnected(color === STATUS.connected);
24
+ }, []);
25
+ const sendResize = useCallback(() => {
26
+ const fit = fitAddonRef.current;
27
+ const ws = wsRef.current;
28
+ const term = termRef.current;
29
+ if (!fit || !term || !ws || ws.readyState !== WebSocket.OPEN) return;
30
+ fit.fit();
31
+ const payload = JSON.stringify({ columns: term.cols, rows: term.rows });
32
+ ws.send("1" + payload);
33
+ }, []);
34
+ const connect = useCallback(() => {
35
+ const term = termRef.current;
36
+ if (!term) return;
37
+ setStatus(STATUS.connecting);
38
+ const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
39
+ const ws = new WebSocket(`${protocol}//${window.location.host}/code/${codeWorkspaceId}/ws`);
40
+ wsRef.current = ws;
41
+ ws.binaryType = "arraybuffer";
42
+ ws.onopen = () => {
43
+ const handshake = JSON.stringify({ AuthToken: "", columns: term.cols, rows: term.rows });
44
+ ws.send(handshake);
45
+ setStatus(STATUS.connected);
46
+ };
47
+ ws.onmessage = (ev) => {
48
+ const data = typeof ev.data === "string" ? ev.data : new TextDecoder().decode(ev.data);
49
+ const type = data[0];
50
+ const payload = data.slice(1);
51
+ switch (type) {
52
+ case "0":
53
+ term.write(payload);
54
+ break;
55
+ case "1":
56
+ document.title = payload || "Code Workspace";
57
+ break;
58
+ case "2":
59
+ break;
60
+ }
61
+ };
62
+ ws.onclose = () => {
63
+ setStatus(STATUS.disconnected);
64
+ retryTimer.current = setTimeout(connect, RECONNECT_INTERVAL);
65
+ };
66
+ ws.onerror = () => {
67
+ ws.close();
68
+ };
69
+ }, [codeWorkspaceId, setStatus]);
70
+ useEffect(() => {
71
+ const term = new Terminal({
72
+ cursorBlink: true,
73
+ fontSize: 16,
74
+ fontFamily: '"Fira Code", "Cascadia Code", "JetBrains Mono", Menlo, monospace',
75
+ theme: {
76
+ background: "#1a1b26",
77
+ foreground: "#a9b1d6",
78
+ cursor: "#c0caf5",
79
+ selectionBackground: "#33467c"
80
+ },
81
+ allowProposedApi: true
82
+ });
83
+ const fitAddon = new FitAddon();
84
+ const searchAddon = new SearchAddon();
85
+ const webLinksAddon = new WebLinksAddon();
86
+ const serializeAddon = new SerializeAddon();
87
+ term.loadAddon(fitAddon);
88
+ term.loadAddon(searchAddon);
89
+ term.loadAddon(webLinksAddon);
90
+ term.loadAddon(serializeAddon);
91
+ termRef.current = term;
92
+ fitAddonRef.current = fitAddon;
93
+ term.open(containerRef.current);
94
+ const style = document.createElement("style");
95
+ style.textContent = ".xterm, .xterm-viewport { background-color: #1a1b26 !important; }";
96
+ containerRef.current.appendChild(style);
97
+ fitAddon.fit();
98
+ term.onData((data) => {
99
+ const ws = wsRef.current;
100
+ if (ws && ws.readyState === WebSocket.OPEN) {
101
+ ws.send("0" + data);
102
+ }
103
+ });
104
+ let resizeTimeout;
105
+ const handleResize = () => {
106
+ clearTimeout(resizeTimeout);
107
+ resizeTimeout = setTimeout(sendResize, 100);
108
+ };
109
+ window.addEventListener("resize", handleResize);
110
+ let cancelled = false;
111
+ (async () => {
112
+ try {
113
+ await ensureCodeWorkspaceContainer(codeWorkspaceId);
114
+ } catch {
115
+ }
116
+ if (!cancelled) connect();
117
+ })();
118
+ return () => {
119
+ cancelled = true;
120
+ clearTimeout(resizeTimeout);
121
+ clearTimeout(retryTimer.current);
122
+ window.removeEventListener("resize", handleResize);
123
+ if (wsRef.current) wsRef.current.close();
124
+ term.dispose();
125
+ };
126
+ }, [connect, sendResize, codeWorkspaceId]);
127
+ const handleReconnect = async () => {
128
+ clearTimeout(retryTimer.current);
129
+ if (wsRef.current) wsRef.current.close();
130
+ try {
131
+ await ensureCodeWorkspaceContainer(codeWorkspaceId);
132
+ } catch {
133
+ }
134
+ connect();
135
+ };
136
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
137
+ /* @__PURE__ */ jsxs("div", { style: { position: "relative", flex: 1, minHeight: 0 }, children: [
138
+ /* @__PURE__ */ jsx("div", { ref: containerRef, className: "mx-4", style: { height: "100%", borderRadius: 6, overflow: "hidden" } }),
139
+ !connected && /* @__PURE__ */ jsx("div", { style: {
140
+ position: "absolute",
141
+ top: "50%",
142
+ left: "50%",
143
+ transform: "translate(-50%, -50%)",
144
+ background: "rgba(255,255,255,0.9)",
145
+ color: "#000",
146
+ padding: "12px 24px",
147
+ borderRadius: 8,
148
+ fontSize: 14,
149
+ fontWeight: 500,
150
+ boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
151
+ zIndex: 10,
152
+ textAlign: "center",
153
+ maxWidth: 320
154
+ }, children: "Loading..." })
155
+ ] }),
156
+ /* @__PURE__ */ jsxs(
157
+ "div",
158
+ {
159
+ style: {
160
+ flexShrink: 0,
161
+ height: 36,
162
+ display: "flex",
163
+ alignItems: "center",
164
+ justifyContent: "space-between",
165
+ padding: "0 16px"
166
+ },
167
+ children: [
168
+ /* @__PURE__ */ jsx("div", {}),
169
+ /* @__PURE__ */ jsx("div", { style: { display: "flex", alignItems: "center", gap: 8 }, children: /* @__PURE__ */ jsxs("button", { onClick: handleReconnect, style: { ...btnStyle, display: "flex", alignItems: "center", gap: 6 }, children: [
170
+ /* @__PURE__ */ jsx(
171
+ "div",
172
+ {
173
+ ref: statusRef,
174
+ style: {
175
+ width: 8,
176
+ height: 8,
177
+ borderRadius: "50%",
178
+ backgroundColor: STATUS.connecting
179
+ }
180
+ }
181
+ ),
182
+ "Reconnect"
183
+ ] }) })
184
+ ]
185
+ }
186
+ )
187
+ ] });
188
+ }
189
+ const btnStyle = {
190
+ background: "transparent",
191
+ border: "1px solid #d1d5db",
192
+ color: "inherit",
193
+ padding: "4px 10px",
194
+ borderRadius: 4,
195
+ cursor: "pointer",
196
+ fontSize: 12,
197
+ fontFamily: "inherit"
198
+ };
199
+ export {
200
+ TerminalView as default
201
+ };
@@ -0,0 +1,224 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useCallback, useState } from 'react';
4
+ import { Terminal } from '@xterm/xterm';
5
+ import { FitAddon } from '@xterm/addon-fit';
6
+ import { SearchAddon } from '@xterm/addon-search';
7
+ import { WebLinksAddon } from '@xterm/addon-web-links';
8
+ import { SerializeAddon } from '@xterm/addon-serialize';
9
+ import { ensureCodeWorkspaceContainer } from './actions.js';
10
+ import '@xterm/xterm/css/xterm.css';
11
+
12
+ const STATUS = { connected: '#22c55e', connecting: '#eab308', disconnected: '#ef4444' };
13
+ const RECONNECT_INTERVAL = 3000;
14
+
15
+ export default function TerminalView({ codeWorkspaceId }) {
16
+ const containerRef = useRef(null);
17
+ const termRef = useRef(null);
18
+ const fitAddonRef = useRef(null);
19
+ const wsRef = useRef(null);
20
+ const retryTimer = useRef(null);
21
+ const statusRef = useRef(null);
22
+ const [connected, setConnected] = useState(false);
23
+
24
+ const setStatus = useCallback((color) => {
25
+ if (statusRef.current) statusRef.current.style.backgroundColor = color;
26
+ setConnected(color === STATUS.connected);
27
+ }, []);
28
+
29
+ const sendResize = useCallback(() => {
30
+ const fit = fitAddonRef.current;
31
+ const ws = wsRef.current;
32
+ const term = termRef.current;
33
+ if (!fit || !term || !ws || ws.readyState !== WebSocket.OPEN) return;
34
+ fit.fit();
35
+ const payload = JSON.stringify({ columns: term.cols, rows: term.rows });
36
+ ws.send('1' + payload);
37
+ }, []);
38
+
39
+ const connect = useCallback(() => {
40
+ const term = termRef.current;
41
+ if (!term) return;
42
+
43
+ setStatus(STATUS.connecting);
44
+
45
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
46
+ const ws = new WebSocket(`${protocol}//${window.location.host}/code/${codeWorkspaceId}/ws`);
47
+ wsRef.current = ws;
48
+
49
+ ws.binaryType = 'arraybuffer';
50
+
51
+ ws.onopen = () => {
52
+ const handshake = JSON.stringify({ AuthToken: '', columns: term.cols, rows: term.rows });
53
+ ws.send(handshake);
54
+ setStatus(STATUS.connected);
55
+ };
56
+
57
+ ws.onmessage = (ev) => {
58
+ const data = typeof ev.data === 'string' ? ev.data : new TextDecoder().decode(ev.data);
59
+ const type = data[0];
60
+ const payload = data.slice(1);
61
+
62
+ switch (type) {
63
+ case '0':
64
+ term.write(payload);
65
+ break;
66
+ case '1':
67
+ document.title = payload || 'Code Workspace';
68
+ break;
69
+ case '2':
70
+ break;
71
+ }
72
+ };
73
+
74
+ ws.onclose = () => {
75
+ setStatus(STATUS.disconnected);
76
+ retryTimer.current = setTimeout(connect, RECONNECT_INTERVAL);
77
+ };
78
+
79
+ ws.onerror = () => {
80
+ ws.close();
81
+ };
82
+ }, [codeWorkspaceId, setStatus]);
83
+
84
+ useEffect(() => {
85
+ const term = new Terminal({
86
+ cursorBlink: true,
87
+ fontSize: 16,
88
+ fontFamily: '"Fira Code", "Cascadia Code", "JetBrains Mono", Menlo, monospace',
89
+ theme: {
90
+ background: '#1a1b26',
91
+ foreground: '#a9b1d6',
92
+ cursor: '#c0caf5',
93
+ selectionBackground: '#33467c',
94
+ },
95
+ allowProposedApi: true,
96
+ });
97
+
98
+ const fitAddon = new FitAddon();
99
+ const searchAddon = new SearchAddon();
100
+ const webLinksAddon = new WebLinksAddon();
101
+ const serializeAddon = new SerializeAddon();
102
+
103
+ term.loadAddon(fitAddon);
104
+ term.loadAddon(searchAddon);
105
+ term.loadAddon(webLinksAddon);
106
+ term.loadAddon(serializeAddon);
107
+
108
+ termRef.current = term;
109
+ fitAddonRef.current = fitAddon;
110
+
111
+ term.open(containerRef.current);
112
+
113
+ const style = document.createElement('style');
114
+ style.textContent = '.xterm, .xterm-viewport { background-color: #1a1b26 !important; }';
115
+ containerRef.current.appendChild(style);
116
+
117
+ fitAddon.fit();
118
+
119
+ term.onData((data) => {
120
+ const ws = wsRef.current;
121
+ if (ws && ws.readyState === WebSocket.OPEN) {
122
+ ws.send('0' + data);
123
+ }
124
+ });
125
+
126
+ let resizeTimeout;
127
+ const handleResize = () => {
128
+ clearTimeout(resizeTimeout);
129
+ resizeTimeout = setTimeout(sendResize, 100);
130
+ };
131
+ window.addEventListener('resize', handleResize);
132
+
133
+ let cancelled = false;
134
+
135
+ (async () => {
136
+ try {
137
+ await ensureCodeWorkspaceContainer(codeWorkspaceId);
138
+ } catch {}
139
+ if (!cancelled) connect();
140
+ })();
141
+
142
+ return () => {
143
+ cancelled = true;
144
+ clearTimeout(resizeTimeout);
145
+ clearTimeout(retryTimer.current);
146
+ window.removeEventListener('resize', handleResize);
147
+ if (wsRef.current) wsRef.current.close();
148
+ term.dispose();
149
+ };
150
+ }, [connect, sendResize, codeWorkspaceId]);
151
+
152
+ const handleReconnect = async () => {
153
+ clearTimeout(retryTimer.current);
154
+ if (wsRef.current) wsRef.current.close();
155
+ try { await ensureCodeWorkspaceContainer(codeWorkspaceId); } catch {}
156
+ connect();
157
+ };
158
+
159
+ return (
160
+ <>
161
+ <div style={{ position: 'relative', flex: 1, minHeight: 0 }}>
162
+ <div ref={containerRef} className="mx-4" style={{ height: '100%', borderRadius: 6, overflow: 'hidden' }} />
163
+ {!connected && (
164
+ <div style={{
165
+ position: 'absolute',
166
+ top: '50%', left: '50%',
167
+ transform: 'translate(-50%, -50%)',
168
+ background: 'rgba(255,255,255,0.9)',
169
+ color: '#000',
170
+ padding: '12px 24px',
171
+ borderRadius: 8,
172
+ fontSize: 14,
173
+ fontWeight: 500,
174
+ boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
175
+ zIndex: 10,
176
+ textAlign: 'center',
177
+ maxWidth: 320,
178
+ }}>
179
+ Loading...
180
+ </div>
181
+ )}
182
+ </div>
183
+
184
+ {/* Toolbar */}
185
+ <div
186
+ style={{
187
+ flexShrink: 0,
188
+ height: 36,
189
+ display: 'flex',
190
+ alignItems: 'center',
191
+ justifyContent: 'space-between',
192
+ padding: '0 16px',
193
+ }}
194
+ >
195
+ <div />
196
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
197
+ <button onClick={handleReconnect} style={{ ...btnStyle, display: 'flex', alignItems: 'center', gap: 6 }}>
198
+ <div
199
+ ref={statusRef}
200
+ style={{
201
+ width: 8,
202
+ height: 8,
203
+ borderRadius: '50%',
204
+ backgroundColor: STATUS.connecting,
205
+ }}
206
+ />
207
+ Reconnect
208
+ </button>
209
+ </div>
210
+ </div>
211
+ </>
212
+ );
213
+ }
214
+
215
+ const btnStyle = {
216
+ background: 'transparent',
217
+ border: '1px solid #d1d5db',
218
+ color: 'inherit',
219
+ padding: '4px 10px',
220
+ borderRadius: 4,
221
+ cursor: 'pointer',
222
+ fontSize: 12,
223
+ fontFamily: 'inherit',
224
+ };
@@ -0,0 +1,80 @@
1
+ import { WebSocketServer, WebSocket } from 'ws';
2
+ import { decode } from 'next-auth/jwt';
3
+ import { getCodeWorkspaceById } from '../db/code-workspaces.js';
4
+
5
+ async function isAuthenticated(req) {
6
+ const cookies = req.headers.cookie || '';
7
+ const secureName = '__Secure-authjs.session-token';
8
+ const plainName = 'authjs.session-token';
9
+ const isSecure = cookies.includes(secureName);
10
+ const name = isSecure ? secureName : plainName;
11
+ const match = cookies.match(new RegExp(`(?:^|;\\s*)${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}=([^;]+)`));
12
+ if (!match) return false;
13
+
14
+ try {
15
+ const token = await decode({
16
+ token: match[1],
17
+ secret: process.env.AUTH_SECRET,
18
+ salt: name,
19
+ });
20
+ return !!token?.sub;
21
+ } catch {
22
+ return false;
23
+ }
24
+ }
25
+
26
+ export function attachCodeProxy(server) {
27
+ const wss = new WebSocketServer({ noServer: true, perMessageDeflate: false });
28
+
29
+ server.on('upgrade', async (req, socket, head) => {
30
+ const match = req.url.match(/^\/code\/([^/]+)\/ws$/);
31
+ if (!match) return;
32
+
33
+ if (!await isAuthenticated(req)) {
34
+ console.log('[ws-proxy] rejected: unauthenticated upgrade');
35
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
36
+ socket.destroy();
37
+ return;
38
+ }
39
+
40
+ const codeWorkspaceId = match[1];
41
+ const codeWorkspace = getCodeWorkspaceById(codeWorkspaceId);
42
+ if (!codeWorkspace) {
43
+ console.log(`[ws-proxy] rejected: unknown workspace ${codeWorkspaceId}`);
44
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
45
+ socket.destroy();
46
+ return;
47
+ }
48
+
49
+ const container = codeWorkspace.containerName;
50
+
51
+ wss.handleUpgrade(req, socket, head, (clientWs) => {
52
+ const backendWs = new WebSocket(`ws://${container}:7681/ws`, 'tty');
53
+
54
+ backendWs.on('open', () => {
55
+ console.log(`[ws-proxy] connected: ${container}`);
56
+ });
57
+
58
+ backendWs.on('message', (data, isBinary) => {
59
+ if (clientWs.readyState === WebSocket.OPEN) {
60
+ clientWs.send(data, { binary: isBinary });
61
+ }
62
+ });
63
+
64
+ clientWs.on('message', (data, isBinary) => {
65
+ if (backendWs.readyState === WebSocket.OPEN) {
66
+ backendWs.send(data, { binary: isBinary });
67
+ }
68
+ });
69
+
70
+ backendWs.on('error', (err) => {
71
+ console.error(`[ws-proxy] backend error: ${err.message}`);
72
+ clientWs.close();
73
+ });
74
+
75
+ backendWs.on('close', () => clientWs.close());
76
+ clientWs.on('error', () => backendWs.close());
77
+ clientWs.on('close', () => backendWs.close());
78
+ });
79
+ });
80
+ }