@burdenoff/vibe-agent 1.0.1 → 1.0.3
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.
- package/README.md +14 -14
- package/dist/app.d.ts +4 -4
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +145 -130
- package/dist/app.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +463 -333
- package/dist/cli.js.map +1 -1
- package/dist/db/schema.d.ts +15 -15
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +65 -62
- package/dist/db/schema.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +18 -18
- package/dist/index.js.map +1 -1
- package/dist/middleware/ModuleAuth.d.ts +2 -2
- package/dist/middleware/ModuleAuth.d.ts.map +1 -1
- package/dist/middleware/ModuleAuth.js +32 -29
- package/dist/middleware/ModuleAuth.js.map +1 -1
- package/dist/middleware/auth.d.ts +1 -1
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +4 -4
- package/dist/middleware/auth.js.map +1 -1
- package/dist/migrations/remove-notes-prompts.d.ts.map +1 -1
- package/dist/migrations/remove-notes-prompts.js +26 -26
- package/dist/migrations/remove-notes-prompts.js.map +1 -1
- package/dist/routes/bookmarks.d.ts +1 -1
- package/dist/routes/bookmarks.d.ts.map +1 -1
- package/dist/routes/bookmarks.js +53 -44
- package/dist/routes/bookmarks.js.map +1 -1
- package/dist/routes/config.d.ts +1 -1
- package/dist/routes/config.d.ts.map +1 -1
- package/dist/routes/config.js +29 -27
- package/dist/routes/config.js.map +1 -1
- package/dist/routes/files.d.ts +1 -1
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +175 -134
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/git.d.ts +1 -1
- package/dist/routes/git.d.ts.map +1 -1
- package/dist/routes/git.js +183 -169
- package/dist/routes/git.js.map +1 -1
- package/dist/routes/moduleRegistry.d.ts +3 -3
- package/dist/routes/moduleRegistry.d.ts.map +1 -1
- package/dist/routes/moduleRegistry.js +58 -58
- package/dist/routes/moduleRegistry.js.map +1 -1
- package/dist/routes/notifications.d.ts +1 -1
- package/dist/routes/notifications.d.ts.map +1 -1
- package/dist/routes/notifications.js +69 -64
- package/dist/routes/notifications.js.map +1 -1
- package/dist/routes/port-forward.d.ts +1 -1
- package/dist/routes/port-forward.d.ts.map +1 -1
- package/dist/routes/port-forward.js +59 -50
- package/dist/routes/port-forward.js.map +1 -1
- package/dist/routes/projects.d.ts +1 -1
- package/dist/routes/projects.d.ts.map +1 -1
- package/dist/routes/projects.js +134 -120
- package/dist/routes/projects.js.map +1 -1
- package/dist/routes/ssh.d.ts +1 -1
- package/dist/routes/ssh.d.ts.map +1 -1
- package/dist/routes/ssh.js +47 -47
- package/dist/routes/ssh.js.map +1 -1
- package/dist/routes/tasks.d.ts +1 -1
- package/dist/routes/tasks.d.ts.map +1 -1
- package/dist/routes/tasks.js +53 -49
- package/dist/routes/tasks.js.map +1 -1
- package/dist/routes/tmux.d.ts +1 -1
- package/dist/routes/tmux.d.ts.map +1 -1
- package/dist/routes/tmux.js +337 -241
- package/dist/routes/tmux.js.map +1 -1
- package/dist/routes/tunnel.d.ts +2 -2
- package/dist/routes/tunnel.d.ts.map +1 -1
- package/dist/routes/tunnel.js +115 -74
- package/dist/routes/tunnel.js.map +1 -1
- package/dist/services/ModulePermissions.d.ts +2 -2
- package/dist/services/ModulePermissions.d.ts.map +1 -1
- package/dist/services/ModulePermissions.js +50 -40
- package/dist/services/ModulePermissions.js.map +1 -1
- package/dist/services/ModuleRegistryService.d.ts +10 -10
- package/dist/services/ModuleRegistryService.d.ts.map +1 -1
- package/dist/services/ModuleRegistryService.js +156 -131
- package/dist/services/ModuleRegistryService.js.map +1 -1
- package/dist/services/agent.service.d.ts.map +1 -1
- package/dist/services/agent.service.js +24 -21
- package/dist/services/agent.service.js.map +1 -1
- package/dist/services/bootstrap.d.ts +1 -1
- package/dist/services/bootstrap.d.ts.map +1 -1
- package/dist/services/bootstrap.js +146 -69
- package/dist/services/bootstrap.js.map +1 -1
- package/dist/services/service-manager.d.ts +2 -2
- package/dist/services/service-manager.d.ts.map +1 -1
- package/dist/services/service-manager.js +75 -63
- package/dist/services/service-manager.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -36,14 +36,14 @@ vibe status
|
|
|
36
36
|
|
|
37
37
|
Create a `.env` file in your working directory or set environment variables:
|
|
38
38
|
|
|
39
|
-
| Variable
|
|
40
|
-
|
|
41
|
-
| `PORT`
|
|
42
|
-
| `NODE_ENV`
|
|
43
|
-
| `CORS_ORIGIN`
|
|
44
|
-
| `AGENT_API_KEY` | API key for authentication
|
|
45
|
-
| `DB_PATH`
|
|
46
|
-
| `AGENT_TUNNEL`
|
|
39
|
+
| Variable | Description | Default |
|
|
40
|
+
| --------------- | ----------------------------- | ------------------------- |
|
|
41
|
+
| `PORT` | Agent server port | `3005` |
|
|
42
|
+
| `NODE_ENV` | Environment | `development` |
|
|
43
|
+
| `CORS_ORIGIN` | Allowed CORS origin | `http://localhost:3000` |
|
|
44
|
+
| `AGENT_API_KEY` | API key for authentication | (auto-generated) |
|
|
45
|
+
| `DB_PATH` | SQLite database path | `./vibecontrols-agent.db` |
|
|
46
|
+
| `AGENT_TUNNEL` | Auto-start cloudflared tunnel | `true` |
|
|
47
47
|
|
|
48
48
|
An `.env.example` file is included in the package for reference.
|
|
49
49
|
|
|
@@ -203,12 +203,12 @@ The agent runs as a Fastify HTTP server with:
|
|
|
203
203
|
|
|
204
204
|
### System Dependencies
|
|
205
205
|
|
|
206
|
-
| Tool
|
|
207
|
-
|
|
208
|
-
| tmux
|
|
209
|
-
| ttyd
|
|
210
|
-
| cloudflared | Tunnel management
|
|
211
|
-
| Node.js 18+ | Runtime
|
|
206
|
+
| Tool | Purpose | Required |
|
|
207
|
+
| ----------- | --------------------- | ----------- |
|
|
208
|
+
| tmux | Terminal multiplexing | Yes |
|
|
209
|
+
| ttyd | Web-based terminal | Recommended |
|
|
210
|
+
| cloudflared | Tunnel management | For tunnels |
|
|
211
|
+
| Node.js 18+ | Runtime | Yes |
|
|
212
212
|
|
|
213
213
|
Run `vibe setup --check` to verify all dependencies.
|
|
214
214
|
|
package/dist/app.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import Fastify from
|
|
2
|
-
import { Server as SocketIOServer } from
|
|
3
|
-
import AgentDatabase from
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import { Server as SocketIOServer } from "socket.io";
|
|
3
|
+
import AgentDatabase from "./db/schema.js";
|
|
4
4
|
/**
|
|
5
5
|
* Get the current agent API key.
|
|
6
6
|
*/
|
|
7
7
|
export declare function getAgentApiKey(): string;
|
|
8
8
|
export declare function startServer(): Promise<Fastify.FastifyInstance<import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>, import("http").IncomingMessage, import("http").ServerResponse<import("http").IncomingMessage>, Fastify.FastifyBaseLogger, Fastify.FastifyTypeProviderDefault>>;
|
|
9
|
-
declare module
|
|
9
|
+
declare module "fastify" {
|
|
10
10
|
interface FastifyInstance {
|
|
11
11
|
db: AgentDatabase;
|
|
12
12
|
io: SocketIOServer;
|
package/dist/app.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAK9B,OAAO,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,aAAa,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAK9B,OAAO,EAAE,MAAM,IAAI,cAAc,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,aAAa,MAAM,gBAAgB,CAAC;AAoC3C;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAWD,wBAAsB,WAAW,uSAigBhC;AAGD,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,EAAE,EAAE,aAAa,CAAC;QAClB,EAAE,EAAE,cAAc,CAAC;KACpB;CACF"}
|
package/dist/app.js
CHANGED
|
@@ -1,37 +1,37 @@
|
|
|
1
|
-
import Fastify from
|
|
2
|
-
import cors from
|
|
3
|
-
import helmet from
|
|
4
|
-
import os from
|
|
5
|
-
import crypto from
|
|
6
|
-
import { Server as SocketIOServer } from
|
|
7
|
-
import { WebSocketServer, WebSocket } from
|
|
8
|
-
import AgentDatabase from
|
|
9
|
-
import { tmuxRoutes } from
|
|
10
|
-
import { sshRoutes } from
|
|
11
|
-
import { portForwardRoutes } from
|
|
12
|
-
import { taskRoutes } from
|
|
13
|
-
import { configRoutes } from
|
|
14
|
-
import { gitRoutes } from
|
|
15
|
-
import { fileRoutes } from
|
|
16
|
-
import { bookmarkRoutes } from
|
|
17
|
-
import { notificationRoutes } from
|
|
18
|
-
import { projectRoutes } from
|
|
19
|
-
import { moduleRegistryRoutes } from
|
|
20
|
-
import { tunnelRoutes, getAgentTunnelUrl, startAgentTunnel, stopAgentTunnel } from
|
|
21
|
-
import { bootstrap, checkDependencies } from
|
|
1
|
+
import Fastify from "fastify";
|
|
2
|
+
import cors from "@fastify/cors";
|
|
3
|
+
import helmet from "@fastify/helmet";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import crypto from "node:crypto";
|
|
6
|
+
import { Server as SocketIOServer } from "socket.io";
|
|
7
|
+
import { WebSocketServer, WebSocket } from "ws";
|
|
8
|
+
import AgentDatabase from "./db/schema.js";
|
|
9
|
+
import { tmuxRoutes } from "./routes/tmux.js";
|
|
10
|
+
import { sshRoutes } from "./routes/ssh.js";
|
|
11
|
+
import { portForwardRoutes } from "./routes/port-forward.js";
|
|
12
|
+
import { taskRoutes } from "./routes/tasks.js";
|
|
13
|
+
import { configRoutes } from "./routes/config.js";
|
|
14
|
+
import { gitRoutes } from "./routes/git.js";
|
|
15
|
+
import { fileRoutes } from "./routes/files.js";
|
|
16
|
+
import { bookmarkRoutes } from "./routes/bookmarks.js";
|
|
17
|
+
import { notificationRoutes } from "./routes/notifications.js";
|
|
18
|
+
import { projectRoutes } from "./routes/projects.js";
|
|
19
|
+
import { moduleRegistryRoutes } from "./routes/moduleRegistry.js";
|
|
20
|
+
import { tunnelRoutes, getAgentTunnelUrl, startAgentTunnel, stopAgentTunnel, } from "./routes/tunnel.js";
|
|
21
|
+
import { bootstrap, checkDependencies } from "./services/bootstrap.js";
|
|
22
22
|
// ── Agent API Key ──────────────────────────────────────────────────────
|
|
23
23
|
// Generated fresh on every agent start/restart. Must be provided via
|
|
24
24
|
// `x-agent-api-key` header (or `apiKey` query param) for all requests
|
|
25
25
|
// except /health and /api/agent-identity.
|
|
26
26
|
// The key is printed to stdout on startup so the user can copy it into
|
|
27
27
|
// the agent configuration in the VibeControls UI.
|
|
28
|
-
let agentApiKey =
|
|
28
|
+
let agentApiKey = "";
|
|
29
29
|
/**
|
|
30
30
|
* Generate a new cryptographically random API key for this agent instance.
|
|
31
31
|
* Called once at startup.
|
|
32
32
|
*/
|
|
33
33
|
function generateAgentApiKey() {
|
|
34
|
-
return `vcak_${crypto.randomBytes(32).toString(
|
|
34
|
+
return `vcak_${crypto.randomBytes(32).toString("base64url")}`;
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
37
|
* Get the current agent API key.
|
|
@@ -45,36 +45,38 @@ export function getAgentApiKey() {
|
|
|
45
45
|
*/
|
|
46
46
|
function getMachineFingerprint() {
|
|
47
47
|
const raw = `${os.hostname()}|${os.platform()}|${os.arch()}`;
|
|
48
|
-
return crypto.createHash(
|
|
48
|
+
return crypto.createHash("sha256").update(raw).digest("hex").substring(0, 16);
|
|
49
49
|
}
|
|
50
50
|
export async function startServer() {
|
|
51
51
|
// Generate a fresh API key for this agent instance
|
|
52
52
|
agentApiKey = generateAgentApiKey();
|
|
53
53
|
const app = Fastify({
|
|
54
|
-
logger: process.env.NODE_ENV ===
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
logger: process.env.NODE_ENV === "development"
|
|
55
|
+
? {
|
|
56
|
+
level: "info",
|
|
57
|
+
serializers: {
|
|
58
|
+
req: (req) => {
|
|
59
|
+
return {
|
|
60
|
+
method: req.method,
|
|
61
|
+
url: req.url,
|
|
62
|
+
host: req.headers.host,
|
|
63
|
+
remoteAddress: req.ip,
|
|
64
|
+
remotePort: req.socket?.remotePort,
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
res: (res) => {
|
|
68
|
+
return {
|
|
69
|
+
statusCode: res.statusCode,
|
|
70
|
+
};
|
|
71
|
+
},
|
|
65
72
|
},
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
statusCode: res.statusCode,
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
} : false,
|
|
73
|
+
}
|
|
74
|
+
: false,
|
|
73
75
|
});
|
|
74
76
|
// Initialize database
|
|
75
|
-
const db = new AgentDatabase(process.env.DB_PATH ||
|
|
77
|
+
const db = new AgentDatabase(process.env.DB_PATH || "./vibecontrols-agent.db");
|
|
76
78
|
// Make db available to routes
|
|
77
|
-
app.decorate(
|
|
79
|
+
app.decorate("db", db);
|
|
78
80
|
// Register plugins
|
|
79
81
|
await app.register(cors, {
|
|
80
82
|
origin: process.env.CORS_ORIGIN || true,
|
|
@@ -86,11 +88,11 @@ export async function startServer() {
|
|
|
86
88
|
frameguard: false, // Allow embedding in iframes (terminal UI)
|
|
87
89
|
});
|
|
88
90
|
// Health check endpoint
|
|
89
|
-
app.get(
|
|
91
|
+
app.get("/health", async () => {
|
|
90
92
|
return {
|
|
91
|
-
status:
|
|
93
|
+
status: "healthy",
|
|
92
94
|
timestamp: new Date().toISOString(),
|
|
93
|
-
version:
|
|
95
|
+
version: "1.0.0",
|
|
94
96
|
uptime: process.uptime(),
|
|
95
97
|
tunnelUrl: getAgentTunnelUrl(),
|
|
96
98
|
};
|
|
@@ -99,7 +101,7 @@ export async function startServer() {
|
|
|
99
101
|
// backend can verify that a tunnel URL reaches the EXPECTED machine.
|
|
100
102
|
// This prevents silent misrouting when a cloudflare quick-tunnel URL
|
|
101
103
|
// is stale or was reassigned.
|
|
102
|
-
app.get(
|
|
104
|
+
app.get("/api/agent-identity", async () => {
|
|
103
105
|
return {
|
|
104
106
|
fingerprint: getMachineFingerprint(),
|
|
105
107
|
hostname: os.hostname(),
|
|
@@ -113,23 +115,28 @@ export async function startServer() {
|
|
|
113
115
|
// This is auth-exempt so the key can be retrieved from the agent itself
|
|
114
116
|
// (e.g. by the agent CLI or during initial setup). In production, access
|
|
115
117
|
// should be restricted to localhost via network policy.
|
|
116
|
-
app.get(
|
|
118
|
+
app.get("/api/agent-api-key", async () => {
|
|
117
119
|
return {
|
|
118
120
|
apiKey: agentApiKey,
|
|
119
121
|
generatedAt: new Date().toISOString(),
|
|
120
|
-
note:
|
|
122
|
+
note: "This key changes every time the agent restarts. Store it in the agent configuration in VibeControls.",
|
|
121
123
|
};
|
|
122
124
|
});
|
|
123
125
|
// ── API Key Authentication ─────────────────────────────────────────────
|
|
124
126
|
// Every request (except allowlisted paths) must include the agent API key
|
|
125
127
|
// via `x-agent-api-key` header or `apiKey` query parameter.
|
|
126
128
|
// The key is generated fresh on every agent start/restart.
|
|
127
|
-
const AUTH_EXEMPT_PATHS = new Set([
|
|
129
|
+
const AUTH_EXEMPT_PATHS = new Set([
|
|
130
|
+
"/health",
|
|
131
|
+
"/api/agent-identity",
|
|
132
|
+
"/api/agent-api-key",
|
|
133
|
+
"/api/agent-tunnel",
|
|
134
|
+
]);
|
|
128
135
|
// Terminal proxy paths are exempt because they're accessed via iframe/WS
|
|
129
136
|
// and the key was already verified when the session was started via the backend.
|
|
130
|
-
const AUTH_EXEMPT_PREFIXES = [
|
|
131
|
-
app.addHook(
|
|
132
|
-
const url = request.url.split(
|
|
137
|
+
const AUTH_EXEMPT_PREFIXES = ["/terminal/"];
|
|
138
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
139
|
+
const url = request.url.split("?")[0]; // strip query string for matching
|
|
133
140
|
// Skip auth for exempt paths
|
|
134
141
|
if (AUTH_EXEMPT_PATHS.has(url))
|
|
135
142
|
return;
|
|
@@ -138,28 +145,28 @@ export async function startServer() {
|
|
|
138
145
|
return;
|
|
139
146
|
}
|
|
140
147
|
// Check API key from header or query param
|
|
141
|
-
const headerKey = request.headers[
|
|
148
|
+
const headerKey = request.headers["x-agent-api-key"];
|
|
142
149
|
const queryKey = request.query?.apiKey;
|
|
143
150
|
const providedKey = headerKey || queryKey;
|
|
144
151
|
if (!providedKey || providedKey !== agentApiKey) {
|
|
145
152
|
return reply.code(401).send({
|
|
146
|
-
error:
|
|
147
|
-
message:
|
|
153
|
+
error: "Unauthorized",
|
|
154
|
+
message: "Invalid or missing API key. Provide the agent API key via x-agent-api-key header.",
|
|
148
155
|
});
|
|
149
156
|
}
|
|
150
157
|
});
|
|
151
158
|
// Register routes
|
|
152
|
-
app.register(tmuxRoutes, { prefix:
|
|
153
|
-
app.register(sshRoutes, { prefix:
|
|
154
|
-
app.register(portForwardRoutes, { prefix:
|
|
155
|
-
app.register(taskRoutes, { prefix:
|
|
156
|
-
app.register(configRoutes, { prefix:
|
|
157
|
-
app.register(gitRoutes, { prefix:
|
|
158
|
-
app.register(fileRoutes, { prefix:
|
|
159
|
-
app.register(bookmarkRoutes, { prefix:
|
|
160
|
-
app.register(notificationRoutes, { prefix:
|
|
161
|
-
app.register(projectRoutes, { prefix:
|
|
162
|
-
app.register(tunnelRoutes, { prefix:
|
|
159
|
+
app.register(tmuxRoutes, { prefix: "/api/tmux" });
|
|
160
|
+
app.register(sshRoutes, { prefix: "/api/ssh" });
|
|
161
|
+
app.register(portForwardRoutes, { prefix: "/api/port-forward" });
|
|
162
|
+
app.register(taskRoutes, { prefix: "/api/tasks" });
|
|
163
|
+
app.register(configRoutes, { prefix: "/api/config" });
|
|
164
|
+
app.register(gitRoutes, { prefix: "/api/git" });
|
|
165
|
+
app.register(fileRoutes, { prefix: "/api/files" });
|
|
166
|
+
app.register(bookmarkRoutes, { prefix: "/api/bookmarks" });
|
|
167
|
+
app.register(notificationRoutes, { prefix: "/api/notifications" });
|
|
168
|
+
app.register(projectRoutes, { prefix: "/api/projects" });
|
|
169
|
+
app.register(tunnelRoutes, { prefix: "/api/tunnel" });
|
|
163
170
|
app.register(moduleRegistryRoutes);
|
|
164
171
|
// --- Terminal proxy: forward /terminal/:sessionId/* → ttyd on localhost ---
|
|
165
172
|
// This lets the single agent tunnel proxy all ttyd sessions, removing the need
|
|
@@ -188,16 +195,16 @@ export async function startServer() {
|
|
|
188
195
|
const fetchInit = {
|
|
189
196
|
method: request.method,
|
|
190
197
|
headers: Object.fromEntries(Object.entries(request.headers)
|
|
191
|
-
.filter(([k, v]) => v != null && ![
|
|
198
|
+
.filter(([k, v]) => v != null && !["host", "connection"].includes(k.toLowerCase()))
|
|
192
199
|
.map(([k, v]) => [k, String(v)])),
|
|
193
200
|
};
|
|
194
|
-
if (request.method !==
|
|
201
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
195
202
|
fetchInit.body = request.body;
|
|
196
203
|
}
|
|
197
204
|
const proxyRes = await fetch(`http://127.0.0.1:${port}${upstreamPath}`, fetchInit);
|
|
198
205
|
reply.code(proxyRes.status);
|
|
199
206
|
for (const [key, value] of proxyRes.headers.entries()) {
|
|
200
|
-
if (![
|
|
207
|
+
if (!["transfer-encoding", "connection"].includes(key.toLowerCase())) {
|
|
201
208
|
reply.header(key, value);
|
|
202
209
|
}
|
|
203
210
|
}
|
|
@@ -205,19 +212,23 @@ export async function startServer() {
|
|
|
205
212
|
return reply.send(buf);
|
|
206
213
|
}
|
|
207
214
|
// HTTP proxy for /terminal/:sessionId and /terminal/:sessionId/*
|
|
208
|
-
instance.all(
|
|
215
|
+
instance.all("/terminal/:sessionId", async (request, reply) => {
|
|
209
216
|
const { sessionId } = request.params;
|
|
210
217
|
const port = resolveTtydPort(sessionId);
|
|
211
218
|
if (!port)
|
|
212
|
-
return reply
|
|
213
|
-
|
|
219
|
+
return reply
|
|
220
|
+
.code(502)
|
|
221
|
+
.send({ error: "Terminal not running for this session" });
|
|
222
|
+
return proxyToTtyd(request, reply, port, "/");
|
|
214
223
|
});
|
|
215
|
-
instance.all(
|
|
224
|
+
instance.all("/terminal/:sessionId/*", async (request, reply) => {
|
|
216
225
|
const { sessionId } = request.params;
|
|
217
|
-
const wildcardPath = request.params[
|
|
226
|
+
const wildcardPath = request.params["*"] || "";
|
|
218
227
|
const port = resolveTtydPort(sessionId);
|
|
219
228
|
if (!port)
|
|
220
|
-
return reply
|
|
229
|
+
return reply
|
|
230
|
+
.code(502)
|
|
231
|
+
.send({ error: "Terminal not running for this session" });
|
|
221
232
|
return proxyToTtyd(request, reply, port, `/${wildcardPath}`);
|
|
222
233
|
});
|
|
223
234
|
// ─── WebSocket proxy for ttyd using the `ws` library ───────────────
|
|
@@ -229,20 +240,20 @@ export async function startServer() {
|
|
|
229
240
|
// Handle sub-protocol negotiation: ttyd requires the 'tty' protocol.
|
|
230
241
|
// When the browser sends Sec-WebSocket-Protocol: tty, we must accept it.
|
|
231
242
|
handleProtocols(protocols) {
|
|
232
|
-
if (protocols.has(
|
|
233
|
-
return
|
|
243
|
+
if (protocols.has("tty"))
|
|
244
|
+
return "tty";
|
|
234
245
|
return false;
|
|
235
246
|
},
|
|
236
247
|
});
|
|
237
|
-
terminalWss.on(
|
|
248
|
+
terminalWss.on("connection", (clientWs, req, ttydPort) => {
|
|
238
249
|
// Connect to the local ttyd WebSocket.
|
|
239
250
|
// CRITICAL: ttyd requires the 'tty' sub-protocol. We must forward it
|
|
240
251
|
// from the client's request headers, otherwise ttyd immediately closes
|
|
241
252
|
// the connection.
|
|
242
|
-
const clientProtocols = req.headers[
|
|
253
|
+
const clientProtocols = req.headers["sec-websocket-protocol"];
|
|
243
254
|
const protocols = clientProtocols
|
|
244
|
-
? clientProtocols.split(
|
|
245
|
-
: [
|
|
255
|
+
? clientProtocols.split(",").map((p) => p.trim())
|
|
256
|
+
: ["tty"]; // Default to 'tty' if browser didn't send it
|
|
246
257
|
const upstreamUrl = `ws://127.0.0.1:${ttydPort}/ws`;
|
|
247
258
|
const upstreamWs = new WebSocket(upstreamUrl, protocols, {
|
|
248
259
|
perMessageDeflate: false,
|
|
@@ -252,7 +263,7 @@ export async function startServer() {
|
|
|
252
263
|
// Buffer messages from upstream until bridge is ready
|
|
253
264
|
const upstreamBuffer = [];
|
|
254
265
|
// When upstream ttyd connection opens, flush buffered messages and bridge
|
|
255
|
-
upstreamWs.on(
|
|
266
|
+
upstreamWs.on("open", () => {
|
|
256
267
|
upstreamAlive = true;
|
|
257
268
|
console.log(`[TerminalProxy] WS bridge established → 127.0.0.1:${ttydPort}/ws (protocol: ${upstreamWs.protocol})`);
|
|
258
269
|
// Flush any buffered messages
|
|
@@ -264,7 +275,7 @@ export async function startServer() {
|
|
|
264
275
|
upstreamBuffer.length = 0;
|
|
265
276
|
});
|
|
266
277
|
// Forward: upstream ttyd → client browser
|
|
267
|
-
upstreamWs.on(
|
|
278
|
+
upstreamWs.on("message", (data, isBinary) => {
|
|
268
279
|
if (!upstreamAlive) {
|
|
269
280
|
// Buffer until upstream is ready (shouldn't happen, but be safe)
|
|
270
281
|
upstreamBuffer.push({ data, isBinary });
|
|
@@ -275,38 +286,40 @@ export async function startServer() {
|
|
|
275
286
|
}
|
|
276
287
|
});
|
|
277
288
|
// Forward: client browser → upstream ttyd
|
|
278
|
-
clientWs.on(
|
|
289
|
+
clientWs.on("message", (data, isBinary) => {
|
|
279
290
|
if (upstreamAlive && upstreamWs.readyState === WebSocket.OPEN) {
|
|
280
291
|
upstreamWs.send(data, { binary: isBinary });
|
|
281
292
|
}
|
|
282
293
|
});
|
|
283
294
|
// Handle close in both directions
|
|
284
|
-
clientWs.on(
|
|
295
|
+
clientWs.on("close", (code, reason) => {
|
|
285
296
|
clientAlive = false;
|
|
286
297
|
console.log(`[TerminalProxy] Client WS closed (code=${code})`);
|
|
287
|
-
if (upstreamWs.readyState === WebSocket.OPEN ||
|
|
288
|
-
upstreamWs.
|
|
298
|
+
if (upstreamWs.readyState === WebSocket.OPEN ||
|
|
299
|
+
upstreamWs.readyState === WebSocket.CONNECTING) {
|
|
300
|
+
upstreamWs.close(1000, "Client disconnected");
|
|
289
301
|
}
|
|
290
302
|
});
|
|
291
|
-
upstreamWs.on(
|
|
303
|
+
upstreamWs.on("close", (code, reason) => {
|
|
292
304
|
upstreamAlive = false;
|
|
293
305
|
console.log(`[TerminalProxy] Upstream ttyd WS closed (code=${code})`);
|
|
294
|
-
if (clientWs.readyState === WebSocket.OPEN ||
|
|
295
|
-
clientWs.
|
|
306
|
+
if (clientWs.readyState === WebSocket.OPEN ||
|
|
307
|
+
clientWs.readyState === WebSocket.CONNECTING) {
|
|
308
|
+
clientWs.close(1000, "Upstream closed");
|
|
296
309
|
}
|
|
297
310
|
});
|
|
298
311
|
// Handle errors
|
|
299
|
-
clientWs.on(
|
|
300
|
-
console.error(
|
|
312
|
+
clientWs.on("error", (err) => {
|
|
313
|
+
console.error("[TerminalProxy] Client WS error:", err.message);
|
|
301
314
|
clientAlive = false;
|
|
302
315
|
if (upstreamWs.readyState === WebSocket.OPEN)
|
|
303
316
|
upstreamWs.close();
|
|
304
317
|
});
|
|
305
|
-
upstreamWs.on(
|
|
306
|
-
console.error(
|
|
318
|
+
upstreamWs.on("error", (err) => {
|
|
319
|
+
console.error("[TerminalProxy] Upstream WS error:", err.message);
|
|
307
320
|
upstreamAlive = false;
|
|
308
321
|
if (clientWs.readyState === WebSocket.OPEN) {
|
|
309
|
-
clientWs.close(1011,
|
|
322
|
+
clientWs.close(1011, "Upstream error");
|
|
310
323
|
}
|
|
311
324
|
});
|
|
312
325
|
});
|
|
@@ -316,47 +329,49 @@ export async function startServer() {
|
|
|
316
329
|
instance.server._resolveTtydPort = resolveTtydPort;
|
|
317
330
|
});
|
|
318
331
|
// --- Agent tunnel management endpoints ---
|
|
319
|
-
app.get(
|
|
332
|
+
app.get("/api/agent-tunnel", async () => {
|
|
320
333
|
return {
|
|
321
334
|
tunnelUrl: getAgentTunnelUrl(),
|
|
322
|
-
status: getAgentTunnelUrl() ?
|
|
335
|
+
status: getAgentTunnelUrl() ? "active" : "inactive",
|
|
323
336
|
};
|
|
324
337
|
});
|
|
325
|
-
app.post(
|
|
338
|
+
app.post("/api/agent-tunnel/start", async (_request, reply) => {
|
|
326
339
|
try {
|
|
327
340
|
const port = Number(process.env.PORT || 3005);
|
|
328
341
|
const url = await startAgentTunnel(port);
|
|
329
|
-
return { tunnelUrl: url, status:
|
|
342
|
+
return { tunnelUrl: url, status: "active" };
|
|
330
343
|
}
|
|
331
344
|
catch (err) {
|
|
332
345
|
return reply.code(500).send({
|
|
333
|
-
error:
|
|
346
|
+
error: "Failed to start agent tunnel",
|
|
334
347
|
details: err instanceof Error ? err.message : String(err),
|
|
335
348
|
});
|
|
336
349
|
}
|
|
337
350
|
});
|
|
338
|
-
app.post(
|
|
351
|
+
app.post("/api/agent-tunnel/stop", async () => {
|
|
339
352
|
stopAgentTunnel();
|
|
340
|
-
return { status:
|
|
353
|
+
return { status: "inactive" };
|
|
341
354
|
});
|
|
342
355
|
// Setup/bootstrap endpoint - check and install dependencies
|
|
343
|
-
app.get(
|
|
356
|
+
app.get("/api/setup/check", async () => {
|
|
344
357
|
return { dependencies: checkDependencies() };
|
|
345
358
|
});
|
|
346
|
-
app.post(
|
|
359
|
+
app.post("/api/setup/install", async () => {
|
|
347
360
|
const results = await bootstrap({ verbose: false });
|
|
348
|
-
const failed = results.filter(r => r.status ===
|
|
361
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
349
362
|
return {
|
|
350
363
|
success: failed.length === 0,
|
|
351
364
|
results,
|
|
352
365
|
message: failed.length === 0
|
|
353
|
-
?
|
|
354
|
-
: `Failed to install: ${failed.map(f => f.tool).join(
|
|
366
|
+
? "All dependencies installed successfully"
|
|
367
|
+
: `Failed to install: ${failed.map((f) => f.tool).join(", ")}`,
|
|
355
368
|
};
|
|
356
369
|
});
|
|
357
370
|
// Version endpoint
|
|
358
|
-
app.get(
|
|
359
|
-
const packageJson = await import(
|
|
371
|
+
app.get("/api/version", async (_request, _reply) => {
|
|
372
|
+
const packageJson = await import("../package.json", {
|
|
373
|
+
assert: { type: "json" },
|
|
374
|
+
});
|
|
360
375
|
return {
|
|
361
376
|
name: packageJson.default.name,
|
|
362
377
|
version: packageJson.default.version,
|
|
@@ -364,48 +379,48 @@ export async function startServer() {
|
|
|
364
379
|
platform: process.platform,
|
|
365
380
|
arch: process.arch,
|
|
366
381
|
uptime: process.uptime(),
|
|
367
|
-
apiVersion:
|
|
382
|
+
apiVersion: "v1",
|
|
368
383
|
};
|
|
369
384
|
});
|
|
370
385
|
// Socket.io for real-time updates
|
|
371
386
|
const io = new SocketIOServer(app.server, {
|
|
372
387
|
cors: {
|
|
373
388
|
origin: process.env.CORS_ORIGIN || true,
|
|
374
|
-
credentials: true
|
|
375
|
-
}
|
|
389
|
+
credentials: true,
|
|
390
|
+
},
|
|
376
391
|
});
|
|
377
|
-
io.on(
|
|
378
|
-
console.log(
|
|
379
|
-
socket.on(
|
|
392
|
+
io.on("connection", (socket) => {
|
|
393
|
+
console.log("Client connected:", socket.id);
|
|
394
|
+
socket.on("subscribe", (channel) => {
|
|
380
395
|
socket.join(channel);
|
|
381
396
|
console.log(`Client ${socket.id} subscribed to ${channel}`);
|
|
382
397
|
});
|
|
383
|
-
socket.on(
|
|
398
|
+
socket.on("unsubscribe", (channel) => {
|
|
384
399
|
socket.leave(channel);
|
|
385
400
|
console.log(`Client ${socket.id} unsubscribed from ${channel}`);
|
|
386
401
|
});
|
|
387
|
-
socket.on(
|
|
388
|
-
console.log(
|
|
402
|
+
socket.on("disconnect", () => {
|
|
403
|
+
console.log("Client disconnected:", socket.id);
|
|
389
404
|
});
|
|
390
405
|
});
|
|
391
406
|
// Make io available to routes
|
|
392
|
-
app.decorate(
|
|
407
|
+
app.decorate("io", io);
|
|
393
408
|
// ── Terminal WebSocket upgrade handler ──────────────────────────────
|
|
394
409
|
// Install this in onReady hook so that:
|
|
395
410
|
// 1. Fastify plugins (including terminalProxy) have initialized
|
|
396
411
|
// 2. Socket.IO has attached its upgrade listener
|
|
397
412
|
// We replace ALL upgrade listeners with a single master handler that
|
|
398
413
|
// routes terminal WS to our WSS and everything else to Socket.IO.
|
|
399
|
-
app.addHook(
|
|
414
|
+
app.addHook("onReady", async () => {
|
|
400
415
|
const terminalWss = app.server._terminalWss;
|
|
401
416
|
const resolveTtydPort = app.server._resolveTtydPort;
|
|
402
417
|
if (terminalWss && resolveTtydPort) {
|
|
403
418
|
// Grab all current upgrade listeners (Socket.IO's handler)
|
|
404
|
-
const existingUpgradeListeners = app.server.listeners(
|
|
419
|
+
const existingUpgradeListeners = app.server.listeners("upgrade").slice();
|
|
405
420
|
// Remove them all
|
|
406
|
-
app.server.removeAllListeners(
|
|
421
|
+
app.server.removeAllListeners("upgrade");
|
|
407
422
|
// Add a single master upgrade handler
|
|
408
|
-
app.server.on(
|
|
423
|
+
app.server.on("upgrade", (req, socket, head) => {
|
|
409
424
|
const match = req.url?.match(/^\/terminal\/([^/]+)\/ws/);
|
|
410
425
|
if (match) {
|
|
411
426
|
// Terminal WebSocket — handle with our WSS
|
|
@@ -413,13 +428,13 @@ export async function startServer() {
|
|
|
413
428
|
const port = resolveTtydPort(sessionId);
|
|
414
429
|
if (!port) {
|
|
415
430
|
console.warn(`[TerminalProxy] No ttyd running for session ${sessionId}, rejecting`);
|
|
416
|
-
socket.write(
|
|
431
|
+
socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
417
432
|
socket.destroy();
|
|
418
433
|
return;
|
|
419
434
|
}
|
|
420
435
|
console.log(`[TerminalProxy] Upgrading WS for session ${sessionId} → ttyd port ${port}`);
|
|
421
436
|
terminalWss.handleUpgrade(req, socket, head, (ws) => {
|
|
422
|
-
terminalWss.emit(
|
|
437
|
+
terminalWss.emit("connection", ws, req, port);
|
|
423
438
|
});
|
|
424
439
|
// Do NOT call other handlers — terminal WS is fully handled
|
|
425
440
|
return;
|
|
@@ -429,14 +444,14 @@ export async function startServer() {
|
|
|
429
444
|
listener.call(app.server, req, socket, head);
|
|
430
445
|
}
|
|
431
446
|
});
|
|
432
|
-
console.log(
|
|
447
|
+
console.log("[App] Terminal WS upgrade handler installed (master handler replaces Socket.IO)");
|
|
433
448
|
}
|
|
434
449
|
else {
|
|
435
|
-
console.warn(
|
|
450
|
+
console.warn("[App] Terminal proxy not initialized — WS upgrade handler not installed");
|
|
436
451
|
}
|
|
437
452
|
});
|
|
438
453
|
// Graceful shutdown
|
|
439
|
-
app.addHook(
|
|
454
|
+
app.addHook("onClose", async () => {
|
|
440
455
|
db.close();
|
|
441
456
|
io.close();
|
|
442
457
|
});
|