diff --git a/apps/harness/Dockerfile b/apps/harness/Dockerfile index 03b4d05..6e2aa2f 100644 --- a/apps/harness/Dockerfile +++ b/apps/harness/Dockerfile @@ -2,7 +2,7 @@ FROM node:20-alpine AS base RUN corepack enable && corepack prepare pnpm@latest --activate FROM base AS deps -RUN apk add --no-cache libc6-compat +RUN apk add --no-cache libc6-compat python3 make gcc g++ linux-headers WORKDIR /app # Copy workspace root config + relevant package.jsons for install @@ -40,7 +40,14 @@ RUN mkdir -p /data/harness && chown nextjs:nodejs /data/harness COPY --from=builder /app/apps/harness/public ./apps/harness/public COPY --from=builder --chown=nextjs:nodejs /app/apps/harness/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/apps/harness/.next/static ./apps/harness/.next/static + +# PTY server dependencies + custom server +COPY --from=builder /app/node_modules/.pnpm/node-pty*/node_modules/node-pty ./node_modules/node-pty +COPY --from=builder /app/node_modules/.pnpm/ws*/node_modules/ws ./node_modules/ws +COPY apps/harness/server.js ./server.js +COPY apps/harness/pty-server.js ./pty-server.js + USER nextjs EXPOSE 3100 ENV PORT=3100 -CMD ["node", "apps/harness/server.js"] +CMD ["node", "server.js"] diff --git a/apps/harness/k8s/base/deployment.yaml b/apps/harness/k8s/base/deployment.yaml index 6d7446b..4d075ff 100644 --- a/apps/harness/k8s/base/deployment.yaml +++ b/apps/harness/k8s/base/deployment.yaml @@ -47,7 +47,7 @@ spec: memory: 256Mi cpu: 100m limits: - memory: 1Gi + memory: 2Gi readinessProbe: httpGet: path: /api/health diff --git a/apps/harness/next.config.js b/apps/harness/next.config.js index 2c7ac9a..0b70f84 100644 --- a/apps/harness/next.config.js +++ b/apps/harness/next.config.js @@ -2,6 +2,7 @@ const nextConfig = { output: "standalone", instrumentationHook: true, + serverExternalPackages: ["node-pty"], }; module.exports = nextConfig; diff --git a/apps/harness/package.json b/apps/harness/package.json index de08158..7c557f8 100644 --- a/apps/harness/package.json +++ b/apps/harness/package.json @@ -11,17 +11,22 @@ }, "dependencies": { "@homelab/db": "workspace:^", + "@xterm/addon-fit": "^0.11.0", + "@xterm/xterm": "^6.0.0", "drizzle-orm": "^0.36.0", "next": "^15.1.0", + "node-pty": "^1.1.0", "postgres": "^3.4.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "ws": "^8.20.0", "yaml": "^2.7.0" }, "devDependencies": { "@types/node": "^22.10.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", + "@types/ws": "^8.18.1", "typescript": "^5.7.0" } } diff --git a/apps/harness/pty-server.js b/apps/harness/pty-server.js new file mode 100644 index 0000000..c682be3 --- /dev/null +++ b/apps/harness/pty-server.js @@ -0,0 +1,169 @@ +// @ts-check +const pty = require("node-pty"); +const { WebSocketServer } = require("ws"); + +const MAX_SESSIONS = 3; +const MAX_SESSION_AGE_MS = 2 * 60 * 60 * 1000; // 2 hours +const CLEANUP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes + +/** @type {Map} */ +const sessions = new Map(); + +/** + * @param {import("http").Server} server + */ +function attachPtyWebSocket(server) { + const wss = new WebSocketServer({ noServer: true }); + + server.on("upgrade", (req, socket, head) => { + const url = new URL(req.url || "/", `http://${req.headers.host}`); + if (url.pathname !== "/ws/pty") return; + + wss.handleUpgrade(req, socket, head, (ws) => { + wss.emit("connection", ws, req); + }); + }); + + wss.on("connection", async (ws, req) => { + const url = new URL(req.url || "/", `http://${req.headers.host}`); + const agentId = url.searchParams.get("agentId"); + const cols = parseInt(url.searchParams.get("cols") || "80", 10); + const rows = parseInt(url.searchParams.get("rows") || "24", 10); + + if (!agentId) { + ws.send(JSON.stringify({ type: "error", message: "agentId required" })); + ws.close(); + return; + } + + if (sessions.size >= MAX_SESSIONS) { + ws.send( + JSON.stringify({ + type: "error", + message: `max ${MAX_SESSIONS} concurrent sessions`, + }), + ); + ws.close(); + return; + } + + // Fetch agent config + env from the Next.js API + const port = process.env.PORT || 3100; + let config; + try { + const res = await fetch( + `http://127.0.0.1:${port}/api/agents/${encodeURIComponent(agentId)}/env`, + ); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + ws.send( + JSON.stringify({ + type: "error", + message: body.error || `agent lookup failed: ${res.status}`, + }), + ); + ws.close(); + return; + } + config = await res.json(); + } catch (err) { + ws.send( + JSON.stringify({ + type: "error", + message: `failed to fetch agent config: ${err.message}`, + }), + ); + ws.close(); + return; + } + + const sessionId = `${agentId}-${Date.now()}`; + let term; + try { + term = pty.spawn(config.command, config.args, { + name: "xterm-256color", + cols, + rows, + cwd: config.workDir, + env: config.env, + }); + } catch (err) { + ws.send( + JSON.stringify({ + type: "error", + message: `failed to spawn PTY: ${err.message}`, + }), + ); + ws.close(); + return; + } + + sessions.set(sessionId, { pty: term, ws, createdAt: Date.now() }); + + ws.send(JSON.stringify({ type: "connected", sessionId })); + + term.onData((data) => { + if (ws.readyState === ws.OPEN) { + ws.send(data); + } + }); + + term.onExit(({ exitCode }) => { + if (ws.readyState === ws.OPEN) { + ws.send(JSON.stringify({ type: "exit", code: exitCode })); + ws.close(); + } + sessions.delete(sessionId); + }); + + ws.on("message", (data) => { + const msg = data.toString(); + // Try to parse as JSON control message + if (msg.startsWith("{")) { + try { + const ctrl = JSON.parse(msg); + if (ctrl.type === "resize" && ctrl.cols && ctrl.rows) { + term.resize(ctrl.cols, ctrl.rows); + return; + } + } catch { + // Not JSON, treat as terminal input + } + } + term.write(msg); + }); + + ws.on("close", () => { + try { + term.kill(); + } catch { + // Already dead + } + sessions.delete(sessionId); + }); + }); + + // Periodic cleanup of stale sessions + setInterval(() => { + const now = Date.now(); + for (const [id, session] of sessions) { + if (now - session.createdAt > MAX_SESSION_AGE_MS) { + try { + session.pty.kill(); + } catch { + // ignore + } + try { + session.ws.close(); + } catch { + // ignore + } + sessions.delete(id); + } + } + }, CLEANUP_INTERVAL_MS); + + return wss; +} + +module.exports = { attachPtyWebSocket }; diff --git a/apps/harness/server.js b/apps/harness/server.js new file mode 100644 index 0000000..5c43248 --- /dev/null +++ b/apps/harness/server.js @@ -0,0 +1,24 @@ +const { createServer } = require("http"); +const { parse } = require("url"); +const next = require("next"); +const { attachPtyWebSocket } = require("./pty-server"); + +const dev = process.env.NODE_ENV !== "production"; +const hostname = process.env.HOSTNAME || "0.0.0.0"; +const port = parseInt(process.env.PORT || "3100", 10); + +const app = next({ dev, hostname, port }); +const handle = app.getRequestHandler(); + +app.prepare().then(() => { + const server = createServer((req, res) => { + const parsedUrl = parse(req.url || "/", true); + handle(req, res, parsedUrl); + }); + + attachPtyWebSocket(server); + + server.listen(port, hostname, () => { + console.log(`> Harness ready on http://${hostname}:${port}`); + }); +}); diff --git a/apps/harness/src/app/api/agents/[id]/env/route.ts b/apps/harness/src/app/api/agents/[id]/env/route.ts new file mode 100644 index 0000000..1a9b7e3 --- /dev/null +++ b/apps/harness/src/app/api/agents/[id]/env/route.ts @@ -0,0 +1,40 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getAgentConfig } from "@/lib/agents"; +import { buildAgentEnv, buildInteractiveCommand } from "@/lib/agent-env"; + +export async function GET( + _request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + // Only allow localhost access + const forwarded = _request.headers.get("x-forwarded-for"); + const host = _request.headers.get("host") ?? ""; + const isLocal = + !forwarded && + (host.startsWith("localhost") || host.startsWith("127.0.0.1")); + if (!isLocal) { + return NextResponse.json({ error: "forbidden" }, { status: 403 }); + } + + const { id } = await params; + const config = await getAgentConfig(id); + if (!config) { + return NextResponse.json({ error: "agent not found" }, { status: 404 }); + } + + const env = await buildAgentEnv(config); + const { command, args } = buildInteractiveCommand(config); + + // Strip process.env inherited values — only send what we explicitly set + const cleanEnv: Record = {}; + for (const [k, v] of Object.entries(env)) { + if (v !== undefined) cleanEnv[k] = v; + } + + return NextResponse.json({ + command, + args, + env: cleanEnv, + workDir: process.env.HARNESS_WORK_DIR || "/data/harness", + }); +} diff --git a/apps/harness/src/components/chat-tab.tsx b/apps/harness/src/components/chat-tab.tsx new file mode 100644 index 0000000..2e89c36 --- /dev/null +++ b/apps/harness/src/components/chat-tab.tsx @@ -0,0 +1,304 @@ +"use client"; + +import { useState, useEffect, useRef, useCallback } from "react"; +import { + tokens, + Label, + Mono, + Btn, + Panel, + PanelHeader, + SearchableDropdown, + DropdownOption, +} from "./harness-design-system"; + +type SessionState = "idle" | "connecting" | "connected" | "disconnected"; + +interface Agent { + id: string; + name: string; + runtime: string; + modelId: string; +} + +export default function ChatTab({ mobile }: { mobile: boolean }) { + const [agents, setAgents] = useState([]); + const [selectedAgentId, setSelectedAgentId] = useState(""); + const [state, setState] = useState("idle"); + + const termRef = useRef(null); + const xtermRef = useRef(null); + const fitRef = useRef(null); + const wsRef = useRef(null); + + // Fetch agents list + useEffect(() => { + fetch("/api/agents") + .then((r) => r.json()) + .then((data) => { + setAgents(data.configs || []); + }) + .catch(() => {}); + }, []); + + const disconnect = useCallback(() => { + if (wsRef.current) { + wsRef.current.close(); + wsRef.current = null; + } + setState("disconnected"); + }, []); + + const connect = useCallback(async () => { + if (!selectedAgentId || !termRef.current) return; + + setState("connecting"); + + // Dynamic import xterm (SSR-safe) + const { Terminal } = await import("@xterm/xterm"); + const { FitAddon } = await import("@xterm/addon-fit"); + // CSS is imported at module level below + await import("@xterm/xterm/css/xterm.css" as any); + + // Clean up previous terminal + if (xtermRef.current) { + xtermRef.current.dispose(); + } + termRef.current.innerHTML = ""; + + const term = new Terminal({ + cursorBlink: true, + fontFamily: tokens.font.mono, + fontSize: 14, + theme: { + background: tokens.color.bg1, + foreground: tokens.color.text0, + cursor: tokens.color.accent, + selectionBackground: "#374151", + }, + }); + + const fit = new FitAddon(); + term.loadAddon(fit); + term.open(termRef.current); + fit.fit(); + + xtermRef.current = term; + fitRef.current = fit; + + const cols = term.cols; + const rows = term.rows; + + const proto = location.protocol === "https:" ? "wss:" : "ws:"; + const ws = new WebSocket( + `${proto}//${location.host}/ws/pty?agentId=${encodeURIComponent(selectedAgentId)}&cols=${cols}&rows=${rows}`, + ); + wsRef.current = ws; + + ws.onopen = () => { + setState("connected"); + }; + + ws.onmessage = (e) => { + const data = e.data; + // Check for JSON control messages + if (typeof data === "string" && data.startsWith("{")) { + try { + const msg = JSON.parse(data); + if (msg.type === "exit") { + term.writeln(`\r\n\x1b[90m[session exited with code ${msg.code}]\x1b[0m`); + setState("disconnected"); + return; + } + if (msg.type === "error") { + term.writeln(`\r\n\x1b[31m[error: ${msg.message}]\x1b[0m`); + setState("disconnected"); + return; + } + if (msg.type === "connected") { + return; + } + } catch { + // Not JSON, render as terminal output + } + } + term.write(data); + }; + + ws.onclose = () => { + setState("disconnected"); + }; + + ws.onerror = () => { + setState("disconnected"); + }; + + // Terminal → WebSocket + term.onData((data: string) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data); + } + }); + + // Resize handling + term.onResize(({ cols, rows }: { cols: number; rows: number }) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(JSON.stringify({ type: "resize", cols, rows })); + } + }); + }, [selectedAgentId]); + + // ResizeObserver for terminal container + useEffect(() => { + const el = termRef.current; + if (!el) return; + + const observer = new ResizeObserver(() => { + if (fitRef.current) { + try { + fitRef.current.fit(); + } catch { + // Terminal may not be ready + } + } + }); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + // Cleanup on unmount + useEffect(() => { + return () => { + if (wsRef.current) wsRef.current.close(); + if (xtermRef.current) xtermRef.current.dispose(); + }; + }, []); + + const agentOptions: DropdownOption[] = agents.map((a) => ({ + value: a.id, + label: `${a.name}`, + sub: `${a.runtime} · ${a.modelId}`, + })); + + const statusColor = + state === "connected" + ? tokens.color.pass + : state === "connecting" + ? tokens.color.warn + : state === "disconnected" + ? tokens.color.fail + : tokens.color.muted; + + const statusLabel = + state === "connected" + ? "CONNECTED" + : state === "connecting" + ? "CONNECTING" + : state === "disconnected" + ? "DISCONNECTED" + : "IDLE"; + + return ( +
+ {/* Header bar */} +
+
+ setSelectedAgentId(Array.isArray(v) ? v[0] : v)} + placeholder="Select agent..." + /> +
+ + {state === "connected" ? ( + + DISCONNECT + + ) : ( + + {state === "connecting" ? "CONNECTING..." : "CONNECT"} + + )} + +
+
+ +
+
+ + {/* Terminal area */} +
+
+ {state === "idle" && ( +
+ + Select an agent and click CONNECT to start an interactive session + +
+ )} +
+
+ ); +} diff --git a/apps/harness/src/components/harness-dashboard.tsx b/apps/harness/src/components/harness-dashboard.tsx index 759e133..cf90283 100644 --- a/apps/harness/src/components/harness-dashboard.tsx +++ b/apps/harness/src/components/harness-dashboard.tsx @@ -6,6 +6,7 @@ import { Btn, Input, Textarea, EvalPip, IterDot, PathCrumb, BackBtn, SearchableDropdown, DropdownOption, } from "./harness-design-system"; +import ChatTab from "./chat-tab"; // ─── TYPES ────────────────────────────────────────────────────────────────────── @@ -1639,6 +1640,7 @@ function ModelsTab({ mobile }: { mobile: boolean }) { const NAV_ITEMS = [ { id: "LOOPS", icon: "⟳", label: "LOOPS" }, + { id: "CHAT", icon: "▸", label: "CHAT" }, { id: "PROJECTS", icon: "◫", label: "PROJECTS" }, { id: "MODELS", icon: "◈", label: "MODELS" }, { id: "KNOWLEDGE", icon: "≡", label: "DOCS" }, @@ -1672,7 +1674,7 @@ function BottomNav({ activeTab, setActiveTab, tasks }: { activeTab: string; setA function TopBar({ activeTab, setActiveTab, tasks, mobile }: { activeTab: string; setActiveTab: (t: string) => void; tasks: Task[]; mobile: boolean }) { const running = tasks.filter(t => t.status === "running").length; const pending = tasks.filter(t => t.status === "pending").length; - const tabs = ["LOOPS", "PROJECTS", "MODELS", "KNOWLEDGE", "HISTORY", "NEW TASK"]; + const tabs = ["LOOPS", "CHAT", "PROJECTS", "MODELS", "KNOWLEDGE", "HISTORY", "NEW TASK"]; return (
@@ -1760,6 +1762,7 @@ export default function HarnessDashboard() {
{activeTab === "LOOPS" && } + {activeTab === "CHAT" && } {activeTab === "PROJECTS" && } {activeTab === "MODELS" && } {activeTab === "KNOWLEDGE" && } diff --git a/apps/harness/src/lib/agent-env.ts b/apps/harness/src/lib/agent-env.ts new file mode 100644 index 0000000..3de7bc3 --- /dev/null +++ b/apps/harness/src/lib/agent-env.ts @@ -0,0 +1,55 @@ +import { AgentConfig, AGENT_RUNTIMES } from "./agents"; +import { getRawCredentialsByProvider, Provider } from "./credentials"; + +const PROVIDER_ENV_VARS: Record = { + anthropic: "ANTHROPIC_API_KEY", + openai: "OPENAI_API_KEY", + google: "GOOGLE_API_KEY", + openrouter: "OPENROUTER_API_KEY", + "opencode-zen": "OPENCODE_ZEN_API_KEY", +}; + +export async function buildAgentEnv( + config: AgentConfig, +): Promise { + const env: NodeJS.ProcessEnv = { ...process.env, TERM: "xterm-256color" }; + + const providersToInject = + config.runtime === "opencode" + ? Object.keys(PROVIDER_ENV_VARS) + : [config.provider]; + + for (const provider of providersToInject) { + const envVar = PROVIDER_ENV_VARS[provider]; + if (!envVar) continue; + const creds = await getRawCredentialsByProvider(provider as Provider); + if (creds.length > 0) { + env[envVar] = creds[0].token; + } + } + + const ghCreds = await getRawCredentialsByProvider("github" as Provider); + if (ghCreds.length > 0) { + env.GITHUB_TOKEN = ghCreds[0].token; + env.GH_TOKEN = ghCreds[0].token; + } + + if (config.env) { + Object.assign(env, config.env); + } + + return env; +} + +export function buildInteractiveCommand( + config: AgentConfig, +): { command: string; args: string[] } { + const runtime = AGENT_RUNTIMES[config.runtime]; + const args: string[] = []; + + if (runtime.modelFlag && config.modelId) { + args.push(runtime.modelFlag, config.modelId); + } + + return { command: runtime.cliCommand, args }; +} diff --git a/apps/harness/src/lib/executor.ts b/apps/harness/src/lib/executor.ts index 1c20e22..3011b14 100644 --- a/apps/harness/src/lib/executor.ts +++ b/apps/harness/src/lib/executor.ts @@ -1,18 +1,10 @@ import { spawn, ChildProcess } from "node:child_process"; import { getAgentConfig, buildAgentCommand, AGENT_RUNTIMES } from "./agents"; -import { getRawCredentialsByProvider, Provider } from "./credentials"; +import { buildAgentEnv } from "./agent-env"; import { ExecutionResult } from "./types"; const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes -const PROVIDER_ENV_VARS: Record = { - anthropic: "ANTHROPIC_API_KEY", - openai: "OPENAI_API_KEY", - google: "GOOGLE_API_KEY", - openrouter: "OPENROUTER_API_KEY", - "opencode-zen": "OPENCODE_ZEN_API_KEY", -}; - const TOKEN_PATTERNS: Record = { "claude-code": { input: /input[_\s]tokens?[:\s]+(\d[\d,]*)/i, @@ -50,31 +42,7 @@ export async function executeAgent(opts: { const command = args[0]; const commandArgs = args.slice(1); - const env: NodeJS.ProcessEnv = { ...process.env }; - - const providersToInject = - config.runtime === "opencode" - ? Object.keys(PROVIDER_ENV_VARS) - : [config.provider]; - - for (const provider of providersToInject) { - const envVar = PROVIDER_ENV_VARS[provider]; - if (!envVar) continue; - const creds = await getRawCredentialsByProvider(provider as Provider); - if (creds.length > 0) { - env[envVar] = creds[0].token; - } - } - - const ghCreds = await getRawCredentialsByProvider("github" as Provider); - if (ghCreds.length > 0) { - env.GITHUB_TOKEN = ghCreds[0].token; - env.GH_TOKEN = ghCreds[0].token; - } - - if (config.env) { - Object.assign(env, config.env); - } + const env = await buildAgentEnv(config); const timeout = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS; const startTime = Date.now(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6e08f9..3105d73 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,12 +75,21 @@ importers: '@homelab/db': specifier: workspace:^ version: link:../../packages/db + '@xterm/addon-fit': + specifier: ^0.11.0 + version: 0.11.0 + '@xterm/xterm': + specifier: ^6.0.0 + version: 6.0.0 drizzle-orm: specifier: ^0.36.0 version: 0.36.4(@opentelemetry/api@1.9.0)(@types/pg@8.15.6)(@types/react@19.2.14)(postgres@3.4.8)(react@19.2.4) next: specifier: ^15.1.0 version: 15.5.14(@opentelemetry/api@1.9.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + node-pty: + specifier: ^1.1.0 + version: 1.1.0 postgres: specifier: ^3.4.0 version: 3.4.8 @@ -90,6 +99,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.2.4(react@19.2.4) + ws: + specifier: ^8.20.0 + version: 8.20.0 yaml: specifier: ^2.7.0 version: 2.8.2 @@ -103,6 +115,9 @@ importers: '@types/react-dom': specifier: ^19.0.0 version: 19.2.3(@types/react@19.2.14) + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 typescript: specifier: ^5.7.0 version: 5.9.3 @@ -1755,6 +1770,9 @@ packages: '@types/tedious@4.0.14': resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + '@typescript-eslint/eslint-plugin@8.57.1': resolution: {integrity: sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -1909,6 +1927,12 @@ packages: cpu: [x64] os: [win32] + '@xterm/addon-fit@0.11.0': + resolution: {integrity: sha512-jYcgT6xtVYhnhgxh3QgYDnnNMYTcf8ElbxxFzX0IZo+vabQqSPAjC3c1wJrKB5E19VwQei89QCiZZP86DCPF7g==} + + '@xterm/xterm@6.0.0': + resolution: {integrity: sha512-TQwDdQGtwwDt+2cgKDLn0IRaSxYu1tSUjgKarSDkUM0ZNiSRXFpjxEsvc/Zgc5kq5omJ+V0a8/kIM2WD3sMOYg==} + accepts@1.3.8: resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} engines: {node: '>= 0.6'} @@ -3108,6 +3132,9 @@ packages: sass: optional: true + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -3121,6 +3148,9 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-pty@1.1.0: + resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3761,6 +3791,18 @@ packages: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} + ws@8.20.0: + resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -5248,6 +5290,10 @@ snapshots: dependencies: '@types/node': 22.19.15 + '@types/ws@8.18.1': + dependencies: + '@types/node': 22.19.15 + '@typescript-eslint/eslint-plugin@8.57.1(@typescript-eslint/parser@8.57.1(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 @@ -5398,6 +5444,10 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@xterm/addon-fit@0.11.0': {} + + '@xterm/xterm@6.0.0': {} + accepts@1.3.8: dependencies: mime-types: 2.1.35 @@ -6735,6 +6785,8 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-addon-api@7.1.1: {} + node-domexception@1.0.0: {} node-exports-info@1.6.0: @@ -6750,6 +6802,10 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-pty@1.1.0: + dependencies: + node-addon-api: 7.1.1 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -7569,6 +7625,8 @@ snapshots: string-width: 4.2.3 strip-ansi: 6.0.1 + ws@8.20.0: {} + xtend@4.0.2: {} y18n@5.0.8: {}