@burdenoff/vibe-agent 1.0.1 → 1.0.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.
- package/README.md +14 -14
- package/dist/app.d.ts +4 -4
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +144 -130
- package/dist/app.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +342 -332
- 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,uSAggBhC;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,27 @@ 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
|
+
]);
|
|
128
134
|
// Terminal proxy paths are exempt because they're accessed via iframe/WS
|
|
129
135
|
// 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(
|
|
136
|
+
const AUTH_EXEMPT_PREFIXES = ["/terminal/"];
|
|
137
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
138
|
+
const url = request.url.split("?")[0]; // strip query string for matching
|
|
133
139
|
// Skip auth for exempt paths
|
|
134
140
|
if (AUTH_EXEMPT_PATHS.has(url))
|
|
135
141
|
return;
|
|
@@ -138,28 +144,28 @@ export async function startServer() {
|
|
|
138
144
|
return;
|
|
139
145
|
}
|
|
140
146
|
// Check API key from header or query param
|
|
141
|
-
const headerKey = request.headers[
|
|
147
|
+
const headerKey = request.headers["x-agent-api-key"];
|
|
142
148
|
const queryKey = request.query?.apiKey;
|
|
143
149
|
const providedKey = headerKey || queryKey;
|
|
144
150
|
if (!providedKey || providedKey !== agentApiKey) {
|
|
145
151
|
return reply.code(401).send({
|
|
146
|
-
error:
|
|
147
|
-
message:
|
|
152
|
+
error: "Unauthorized",
|
|
153
|
+
message: "Invalid or missing API key. Provide the agent API key via x-agent-api-key header.",
|
|
148
154
|
});
|
|
149
155
|
}
|
|
150
156
|
});
|
|
151
157
|
// 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:
|
|
158
|
+
app.register(tmuxRoutes, { prefix: "/api/tmux" });
|
|
159
|
+
app.register(sshRoutes, { prefix: "/api/ssh" });
|
|
160
|
+
app.register(portForwardRoutes, { prefix: "/api/port-forward" });
|
|
161
|
+
app.register(taskRoutes, { prefix: "/api/tasks" });
|
|
162
|
+
app.register(configRoutes, { prefix: "/api/config" });
|
|
163
|
+
app.register(gitRoutes, { prefix: "/api/git" });
|
|
164
|
+
app.register(fileRoutes, { prefix: "/api/files" });
|
|
165
|
+
app.register(bookmarkRoutes, { prefix: "/api/bookmarks" });
|
|
166
|
+
app.register(notificationRoutes, { prefix: "/api/notifications" });
|
|
167
|
+
app.register(projectRoutes, { prefix: "/api/projects" });
|
|
168
|
+
app.register(tunnelRoutes, { prefix: "/api/tunnel" });
|
|
163
169
|
app.register(moduleRegistryRoutes);
|
|
164
170
|
// --- Terminal proxy: forward /terminal/:sessionId/* → ttyd on localhost ---
|
|
165
171
|
// This lets the single agent tunnel proxy all ttyd sessions, removing the need
|
|
@@ -188,16 +194,16 @@ export async function startServer() {
|
|
|
188
194
|
const fetchInit = {
|
|
189
195
|
method: request.method,
|
|
190
196
|
headers: Object.fromEntries(Object.entries(request.headers)
|
|
191
|
-
.filter(([k, v]) => v != null && ![
|
|
197
|
+
.filter(([k, v]) => v != null && !["host", "connection"].includes(k.toLowerCase()))
|
|
192
198
|
.map(([k, v]) => [k, String(v)])),
|
|
193
199
|
};
|
|
194
|
-
if (request.method !==
|
|
200
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
195
201
|
fetchInit.body = request.body;
|
|
196
202
|
}
|
|
197
203
|
const proxyRes = await fetch(`http://127.0.0.1:${port}${upstreamPath}`, fetchInit);
|
|
198
204
|
reply.code(proxyRes.status);
|
|
199
205
|
for (const [key, value] of proxyRes.headers.entries()) {
|
|
200
|
-
if (![
|
|
206
|
+
if (!["transfer-encoding", "connection"].includes(key.toLowerCase())) {
|
|
201
207
|
reply.header(key, value);
|
|
202
208
|
}
|
|
203
209
|
}
|
|
@@ -205,19 +211,23 @@ export async function startServer() {
|
|
|
205
211
|
return reply.send(buf);
|
|
206
212
|
}
|
|
207
213
|
// HTTP proxy for /terminal/:sessionId and /terminal/:sessionId/*
|
|
208
|
-
instance.all(
|
|
214
|
+
instance.all("/terminal/:sessionId", async (request, reply) => {
|
|
209
215
|
const { sessionId } = request.params;
|
|
210
216
|
const port = resolveTtydPort(sessionId);
|
|
211
217
|
if (!port)
|
|
212
|
-
return reply
|
|
213
|
-
|
|
218
|
+
return reply
|
|
219
|
+
.code(502)
|
|
220
|
+
.send({ error: "Terminal not running for this session" });
|
|
221
|
+
return proxyToTtyd(request, reply, port, "/");
|
|
214
222
|
});
|
|
215
|
-
instance.all(
|
|
223
|
+
instance.all("/terminal/:sessionId/*", async (request, reply) => {
|
|
216
224
|
const { sessionId } = request.params;
|
|
217
|
-
const wildcardPath = request.params[
|
|
225
|
+
const wildcardPath = request.params["*"] || "";
|
|
218
226
|
const port = resolveTtydPort(sessionId);
|
|
219
227
|
if (!port)
|
|
220
|
-
return reply
|
|
228
|
+
return reply
|
|
229
|
+
.code(502)
|
|
230
|
+
.send({ error: "Terminal not running for this session" });
|
|
221
231
|
return proxyToTtyd(request, reply, port, `/${wildcardPath}`);
|
|
222
232
|
});
|
|
223
233
|
// ─── WebSocket proxy for ttyd using the `ws` library ───────────────
|
|
@@ -229,20 +239,20 @@ export async function startServer() {
|
|
|
229
239
|
// Handle sub-protocol negotiation: ttyd requires the 'tty' protocol.
|
|
230
240
|
// When the browser sends Sec-WebSocket-Protocol: tty, we must accept it.
|
|
231
241
|
handleProtocols(protocols) {
|
|
232
|
-
if (protocols.has(
|
|
233
|
-
return
|
|
242
|
+
if (protocols.has("tty"))
|
|
243
|
+
return "tty";
|
|
234
244
|
return false;
|
|
235
245
|
},
|
|
236
246
|
});
|
|
237
|
-
terminalWss.on(
|
|
247
|
+
terminalWss.on("connection", (clientWs, req, ttydPort) => {
|
|
238
248
|
// Connect to the local ttyd WebSocket.
|
|
239
249
|
// CRITICAL: ttyd requires the 'tty' sub-protocol. We must forward it
|
|
240
250
|
// from the client's request headers, otherwise ttyd immediately closes
|
|
241
251
|
// the connection.
|
|
242
|
-
const clientProtocols = req.headers[
|
|
252
|
+
const clientProtocols = req.headers["sec-websocket-protocol"];
|
|
243
253
|
const protocols = clientProtocols
|
|
244
|
-
? clientProtocols.split(
|
|
245
|
-
: [
|
|
254
|
+
? clientProtocols.split(",").map((p) => p.trim())
|
|
255
|
+
: ["tty"]; // Default to 'tty' if browser didn't send it
|
|
246
256
|
const upstreamUrl = `ws://127.0.0.1:${ttydPort}/ws`;
|
|
247
257
|
const upstreamWs = new WebSocket(upstreamUrl, protocols, {
|
|
248
258
|
perMessageDeflate: false,
|
|
@@ -252,7 +262,7 @@ export async function startServer() {
|
|
|
252
262
|
// Buffer messages from upstream until bridge is ready
|
|
253
263
|
const upstreamBuffer = [];
|
|
254
264
|
// When upstream ttyd connection opens, flush buffered messages and bridge
|
|
255
|
-
upstreamWs.on(
|
|
265
|
+
upstreamWs.on("open", () => {
|
|
256
266
|
upstreamAlive = true;
|
|
257
267
|
console.log(`[TerminalProxy] WS bridge established → 127.0.0.1:${ttydPort}/ws (protocol: ${upstreamWs.protocol})`);
|
|
258
268
|
// Flush any buffered messages
|
|
@@ -264,7 +274,7 @@ export async function startServer() {
|
|
|
264
274
|
upstreamBuffer.length = 0;
|
|
265
275
|
});
|
|
266
276
|
// Forward: upstream ttyd → client browser
|
|
267
|
-
upstreamWs.on(
|
|
277
|
+
upstreamWs.on("message", (data, isBinary) => {
|
|
268
278
|
if (!upstreamAlive) {
|
|
269
279
|
// Buffer until upstream is ready (shouldn't happen, but be safe)
|
|
270
280
|
upstreamBuffer.push({ data, isBinary });
|
|
@@ -275,38 +285,40 @@ export async function startServer() {
|
|
|
275
285
|
}
|
|
276
286
|
});
|
|
277
287
|
// Forward: client browser → upstream ttyd
|
|
278
|
-
clientWs.on(
|
|
288
|
+
clientWs.on("message", (data, isBinary) => {
|
|
279
289
|
if (upstreamAlive && upstreamWs.readyState === WebSocket.OPEN) {
|
|
280
290
|
upstreamWs.send(data, { binary: isBinary });
|
|
281
291
|
}
|
|
282
292
|
});
|
|
283
293
|
// Handle close in both directions
|
|
284
|
-
clientWs.on(
|
|
294
|
+
clientWs.on("close", (code, reason) => {
|
|
285
295
|
clientAlive = false;
|
|
286
296
|
console.log(`[TerminalProxy] Client WS closed (code=${code})`);
|
|
287
|
-
if (upstreamWs.readyState === WebSocket.OPEN ||
|
|
288
|
-
upstreamWs.
|
|
297
|
+
if (upstreamWs.readyState === WebSocket.OPEN ||
|
|
298
|
+
upstreamWs.readyState === WebSocket.CONNECTING) {
|
|
299
|
+
upstreamWs.close(1000, "Client disconnected");
|
|
289
300
|
}
|
|
290
301
|
});
|
|
291
|
-
upstreamWs.on(
|
|
302
|
+
upstreamWs.on("close", (code, reason) => {
|
|
292
303
|
upstreamAlive = false;
|
|
293
304
|
console.log(`[TerminalProxy] Upstream ttyd WS closed (code=${code})`);
|
|
294
|
-
if (clientWs.readyState === WebSocket.OPEN ||
|
|
295
|
-
clientWs.
|
|
305
|
+
if (clientWs.readyState === WebSocket.OPEN ||
|
|
306
|
+
clientWs.readyState === WebSocket.CONNECTING) {
|
|
307
|
+
clientWs.close(1000, "Upstream closed");
|
|
296
308
|
}
|
|
297
309
|
});
|
|
298
310
|
// Handle errors
|
|
299
|
-
clientWs.on(
|
|
300
|
-
console.error(
|
|
311
|
+
clientWs.on("error", (err) => {
|
|
312
|
+
console.error("[TerminalProxy] Client WS error:", err.message);
|
|
301
313
|
clientAlive = false;
|
|
302
314
|
if (upstreamWs.readyState === WebSocket.OPEN)
|
|
303
315
|
upstreamWs.close();
|
|
304
316
|
});
|
|
305
|
-
upstreamWs.on(
|
|
306
|
-
console.error(
|
|
317
|
+
upstreamWs.on("error", (err) => {
|
|
318
|
+
console.error("[TerminalProxy] Upstream WS error:", err.message);
|
|
307
319
|
upstreamAlive = false;
|
|
308
320
|
if (clientWs.readyState === WebSocket.OPEN) {
|
|
309
|
-
clientWs.close(1011,
|
|
321
|
+
clientWs.close(1011, "Upstream error");
|
|
310
322
|
}
|
|
311
323
|
});
|
|
312
324
|
});
|
|
@@ -316,47 +328,49 @@ export async function startServer() {
|
|
|
316
328
|
instance.server._resolveTtydPort = resolveTtydPort;
|
|
317
329
|
});
|
|
318
330
|
// --- Agent tunnel management endpoints ---
|
|
319
|
-
app.get(
|
|
331
|
+
app.get("/api/agent-tunnel", async () => {
|
|
320
332
|
return {
|
|
321
333
|
tunnelUrl: getAgentTunnelUrl(),
|
|
322
|
-
status: getAgentTunnelUrl() ?
|
|
334
|
+
status: getAgentTunnelUrl() ? "active" : "inactive",
|
|
323
335
|
};
|
|
324
336
|
});
|
|
325
|
-
app.post(
|
|
337
|
+
app.post("/api/agent-tunnel/start", async (_request, reply) => {
|
|
326
338
|
try {
|
|
327
339
|
const port = Number(process.env.PORT || 3005);
|
|
328
340
|
const url = await startAgentTunnel(port);
|
|
329
|
-
return { tunnelUrl: url, status:
|
|
341
|
+
return { tunnelUrl: url, status: "active" };
|
|
330
342
|
}
|
|
331
343
|
catch (err) {
|
|
332
344
|
return reply.code(500).send({
|
|
333
|
-
error:
|
|
345
|
+
error: "Failed to start agent tunnel",
|
|
334
346
|
details: err instanceof Error ? err.message : String(err),
|
|
335
347
|
});
|
|
336
348
|
}
|
|
337
349
|
});
|
|
338
|
-
app.post(
|
|
350
|
+
app.post("/api/agent-tunnel/stop", async () => {
|
|
339
351
|
stopAgentTunnel();
|
|
340
|
-
return { status:
|
|
352
|
+
return { status: "inactive" };
|
|
341
353
|
});
|
|
342
354
|
// Setup/bootstrap endpoint - check and install dependencies
|
|
343
|
-
app.get(
|
|
355
|
+
app.get("/api/setup/check", async () => {
|
|
344
356
|
return { dependencies: checkDependencies() };
|
|
345
357
|
});
|
|
346
|
-
app.post(
|
|
358
|
+
app.post("/api/setup/install", async () => {
|
|
347
359
|
const results = await bootstrap({ verbose: false });
|
|
348
|
-
const failed = results.filter(r => r.status ===
|
|
360
|
+
const failed = results.filter((r) => r.status === "failed");
|
|
349
361
|
return {
|
|
350
362
|
success: failed.length === 0,
|
|
351
363
|
results,
|
|
352
364
|
message: failed.length === 0
|
|
353
|
-
?
|
|
354
|
-
: `Failed to install: ${failed.map(f => f.tool).join(
|
|
365
|
+
? "All dependencies installed successfully"
|
|
366
|
+
: `Failed to install: ${failed.map((f) => f.tool).join(", ")}`,
|
|
355
367
|
};
|
|
356
368
|
});
|
|
357
369
|
// Version endpoint
|
|
358
|
-
app.get(
|
|
359
|
-
const packageJson = await import(
|
|
370
|
+
app.get("/api/version", async (_request, _reply) => {
|
|
371
|
+
const packageJson = await import("../package.json", {
|
|
372
|
+
assert: { type: "json" },
|
|
373
|
+
});
|
|
360
374
|
return {
|
|
361
375
|
name: packageJson.default.name,
|
|
362
376
|
version: packageJson.default.version,
|
|
@@ -364,48 +378,48 @@ export async function startServer() {
|
|
|
364
378
|
platform: process.platform,
|
|
365
379
|
arch: process.arch,
|
|
366
380
|
uptime: process.uptime(),
|
|
367
|
-
apiVersion:
|
|
381
|
+
apiVersion: "v1",
|
|
368
382
|
};
|
|
369
383
|
});
|
|
370
384
|
// Socket.io for real-time updates
|
|
371
385
|
const io = new SocketIOServer(app.server, {
|
|
372
386
|
cors: {
|
|
373
387
|
origin: process.env.CORS_ORIGIN || true,
|
|
374
|
-
credentials: true
|
|
375
|
-
}
|
|
388
|
+
credentials: true,
|
|
389
|
+
},
|
|
376
390
|
});
|
|
377
|
-
io.on(
|
|
378
|
-
console.log(
|
|
379
|
-
socket.on(
|
|
391
|
+
io.on("connection", (socket) => {
|
|
392
|
+
console.log("Client connected:", socket.id);
|
|
393
|
+
socket.on("subscribe", (channel) => {
|
|
380
394
|
socket.join(channel);
|
|
381
395
|
console.log(`Client ${socket.id} subscribed to ${channel}`);
|
|
382
396
|
});
|
|
383
|
-
socket.on(
|
|
397
|
+
socket.on("unsubscribe", (channel) => {
|
|
384
398
|
socket.leave(channel);
|
|
385
399
|
console.log(`Client ${socket.id} unsubscribed from ${channel}`);
|
|
386
400
|
});
|
|
387
|
-
socket.on(
|
|
388
|
-
console.log(
|
|
401
|
+
socket.on("disconnect", () => {
|
|
402
|
+
console.log("Client disconnected:", socket.id);
|
|
389
403
|
});
|
|
390
404
|
});
|
|
391
405
|
// Make io available to routes
|
|
392
|
-
app.decorate(
|
|
406
|
+
app.decorate("io", io);
|
|
393
407
|
// ── Terminal WebSocket upgrade handler ──────────────────────────────
|
|
394
408
|
// Install this in onReady hook so that:
|
|
395
409
|
// 1. Fastify plugins (including terminalProxy) have initialized
|
|
396
410
|
// 2. Socket.IO has attached its upgrade listener
|
|
397
411
|
// We replace ALL upgrade listeners with a single master handler that
|
|
398
412
|
// routes terminal WS to our WSS and everything else to Socket.IO.
|
|
399
|
-
app.addHook(
|
|
413
|
+
app.addHook("onReady", async () => {
|
|
400
414
|
const terminalWss = app.server._terminalWss;
|
|
401
415
|
const resolveTtydPort = app.server._resolveTtydPort;
|
|
402
416
|
if (terminalWss && resolveTtydPort) {
|
|
403
417
|
// Grab all current upgrade listeners (Socket.IO's handler)
|
|
404
|
-
const existingUpgradeListeners = app.server.listeners(
|
|
418
|
+
const existingUpgradeListeners = app.server.listeners("upgrade").slice();
|
|
405
419
|
// Remove them all
|
|
406
|
-
app.server.removeAllListeners(
|
|
420
|
+
app.server.removeAllListeners("upgrade");
|
|
407
421
|
// Add a single master upgrade handler
|
|
408
|
-
app.server.on(
|
|
422
|
+
app.server.on("upgrade", (req, socket, head) => {
|
|
409
423
|
const match = req.url?.match(/^\/terminal\/([^/]+)\/ws/);
|
|
410
424
|
if (match) {
|
|
411
425
|
// Terminal WebSocket — handle with our WSS
|
|
@@ -413,13 +427,13 @@ export async function startServer() {
|
|
|
413
427
|
const port = resolveTtydPort(sessionId);
|
|
414
428
|
if (!port) {
|
|
415
429
|
console.warn(`[TerminalProxy] No ttyd running for session ${sessionId}, rejecting`);
|
|
416
|
-
socket.write(
|
|
430
|
+
socket.write("HTTP/1.1 502 Bad Gateway\r\n\r\n");
|
|
417
431
|
socket.destroy();
|
|
418
432
|
return;
|
|
419
433
|
}
|
|
420
434
|
console.log(`[TerminalProxy] Upgrading WS for session ${sessionId} → ttyd port ${port}`);
|
|
421
435
|
terminalWss.handleUpgrade(req, socket, head, (ws) => {
|
|
422
|
-
terminalWss.emit(
|
|
436
|
+
terminalWss.emit("connection", ws, req, port);
|
|
423
437
|
});
|
|
424
438
|
// Do NOT call other handlers — terminal WS is fully handled
|
|
425
439
|
return;
|
|
@@ -429,14 +443,14 @@ export async function startServer() {
|
|
|
429
443
|
listener.call(app.server, req, socket, head);
|
|
430
444
|
}
|
|
431
445
|
});
|
|
432
|
-
console.log(
|
|
446
|
+
console.log("[App] Terminal WS upgrade handler installed (master handler replaces Socket.IO)");
|
|
433
447
|
}
|
|
434
448
|
else {
|
|
435
|
-
console.warn(
|
|
449
|
+
console.warn("[App] Terminal proxy not initialized — WS upgrade handler not installed");
|
|
436
450
|
}
|
|
437
451
|
});
|
|
438
452
|
// Graceful shutdown
|
|
439
|
-
app.addHook(
|
|
453
|
+
app.addHook("onClose", async () => {
|
|
440
454
|
db.close();
|
|
441
455
|
io.close();
|
|
442
456
|
});
|