@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.
Files changed (94) hide show
  1. package/README.md +14 -14
  2. package/dist/app.d.ts +4 -4
  3. package/dist/app.d.ts.map +1 -1
  4. package/dist/app.js +145 -130
  5. package/dist/app.js.map +1 -1
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.js +463 -333
  8. package/dist/cli.js.map +1 -1
  9. package/dist/db/schema.d.ts +15 -15
  10. package/dist/db/schema.d.ts.map +1 -1
  11. package/dist/db/schema.js +65 -62
  12. package/dist/db/schema.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +18 -18
  15. package/dist/index.js.map +1 -1
  16. package/dist/middleware/ModuleAuth.d.ts +2 -2
  17. package/dist/middleware/ModuleAuth.d.ts.map +1 -1
  18. package/dist/middleware/ModuleAuth.js +32 -29
  19. package/dist/middleware/ModuleAuth.js.map +1 -1
  20. package/dist/middleware/auth.d.ts +1 -1
  21. package/dist/middleware/auth.d.ts.map +1 -1
  22. package/dist/middleware/auth.js +4 -4
  23. package/dist/middleware/auth.js.map +1 -1
  24. package/dist/migrations/remove-notes-prompts.d.ts.map +1 -1
  25. package/dist/migrations/remove-notes-prompts.js +26 -26
  26. package/dist/migrations/remove-notes-prompts.js.map +1 -1
  27. package/dist/routes/bookmarks.d.ts +1 -1
  28. package/dist/routes/bookmarks.d.ts.map +1 -1
  29. package/dist/routes/bookmarks.js +53 -44
  30. package/dist/routes/bookmarks.js.map +1 -1
  31. package/dist/routes/config.d.ts +1 -1
  32. package/dist/routes/config.d.ts.map +1 -1
  33. package/dist/routes/config.js +29 -27
  34. package/dist/routes/config.js.map +1 -1
  35. package/dist/routes/files.d.ts +1 -1
  36. package/dist/routes/files.d.ts.map +1 -1
  37. package/dist/routes/files.js +175 -134
  38. package/dist/routes/files.js.map +1 -1
  39. package/dist/routes/git.d.ts +1 -1
  40. package/dist/routes/git.d.ts.map +1 -1
  41. package/dist/routes/git.js +183 -169
  42. package/dist/routes/git.js.map +1 -1
  43. package/dist/routes/moduleRegistry.d.ts +3 -3
  44. package/dist/routes/moduleRegistry.d.ts.map +1 -1
  45. package/dist/routes/moduleRegistry.js +58 -58
  46. package/dist/routes/moduleRegistry.js.map +1 -1
  47. package/dist/routes/notifications.d.ts +1 -1
  48. package/dist/routes/notifications.d.ts.map +1 -1
  49. package/dist/routes/notifications.js +69 -64
  50. package/dist/routes/notifications.js.map +1 -1
  51. package/dist/routes/port-forward.d.ts +1 -1
  52. package/dist/routes/port-forward.d.ts.map +1 -1
  53. package/dist/routes/port-forward.js +59 -50
  54. package/dist/routes/port-forward.js.map +1 -1
  55. package/dist/routes/projects.d.ts +1 -1
  56. package/dist/routes/projects.d.ts.map +1 -1
  57. package/dist/routes/projects.js +134 -120
  58. package/dist/routes/projects.js.map +1 -1
  59. package/dist/routes/ssh.d.ts +1 -1
  60. package/dist/routes/ssh.d.ts.map +1 -1
  61. package/dist/routes/ssh.js +47 -47
  62. package/dist/routes/ssh.js.map +1 -1
  63. package/dist/routes/tasks.d.ts +1 -1
  64. package/dist/routes/tasks.d.ts.map +1 -1
  65. package/dist/routes/tasks.js +53 -49
  66. package/dist/routes/tasks.js.map +1 -1
  67. package/dist/routes/tmux.d.ts +1 -1
  68. package/dist/routes/tmux.d.ts.map +1 -1
  69. package/dist/routes/tmux.js +337 -241
  70. package/dist/routes/tmux.js.map +1 -1
  71. package/dist/routes/tunnel.d.ts +2 -2
  72. package/dist/routes/tunnel.d.ts.map +1 -1
  73. package/dist/routes/tunnel.js +115 -74
  74. package/dist/routes/tunnel.js.map +1 -1
  75. package/dist/services/ModulePermissions.d.ts +2 -2
  76. package/dist/services/ModulePermissions.d.ts.map +1 -1
  77. package/dist/services/ModulePermissions.js +50 -40
  78. package/dist/services/ModulePermissions.js.map +1 -1
  79. package/dist/services/ModuleRegistryService.d.ts +10 -10
  80. package/dist/services/ModuleRegistryService.d.ts.map +1 -1
  81. package/dist/services/ModuleRegistryService.js +156 -131
  82. package/dist/services/ModuleRegistryService.js.map +1 -1
  83. package/dist/services/agent.service.d.ts.map +1 -1
  84. package/dist/services/agent.service.js +24 -21
  85. package/dist/services/agent.service.js.map +1 -1
  86. package/dist/services/bootstrap.d.ts +1 -1
  87. package/dist/services/bootstrap.d.ts.map +1 -1
  88. package/dist/services/bootstrap.js +146 -69
  89. package/dist/services/bootstrap.js.map +1 -1
  90. package/dist/services/service-manager.d.ts +2 -2
  91. package/dist/services/service-manager.d.ts.map +1 -1
  92. package/dist/services/service-manager.js +75 -63
  93. package/dist/services/service-manager.js.map +1 -1
  94. 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 | 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` |
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 | 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 |
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 'fastify';
2
- import { Server as SocketIOServer } from 'socket.io';
3
- import AgentDatabase from './db/schema.js';
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 'fastify' {
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;AA+B3C;;GAEG;AACH,wBAAgB,cAAc,IAAI,MAAM,CAEvC;AAWD,wBAAsB,WAAW,uSA4bhC;AAGD,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,eAAe;QACvB,EAAE,EAAE,aAAa,CAAC;QAClB,EAAE,EAAE,cAAc,CAAC;KACpB;CACF"}
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 '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';
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('base64url')}`;
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('sha256').update(raw).digest('hex').substring(0, 16);
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 === 'development' ? {
55
- level: 'info',
56
- serializers: {
57
- req: (req) => {
58
- return {
59
- method: req.method,
60
- url: req.url,
61
- host: req.headers.host,
62
- remoteAddress: req.ip,
63
- remotePort: req.socket?.remotePort,
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
- res: (res) => {
67
- return {
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 || './vibecontrols-agent.db');
77
+ const db = new AgentDatabase(process.env.DB_PATH || "./vibecontrols-agent.db");
76
78
  // Make db available to routes
77
- app.decorate('db', db);
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('/health', async () => {
91
+ app.get("/health", async () => {
90
92
  return {
91
- status: 'healthy',
93
+ status: "healthy",
92
94
  timestamp: new Date().toISOString(),
93
- version: '1.0.0',
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('/api/agent-identity', async () => {
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('/api/agent-api-key', async () => {
118
+ app.get("/api/agent-api-key", async () => {
117
119
  return {
118
120
  apiKey: agentApiKey,
119
121
  generatedAt: new Date().toISOString(),
120
- note: 'This key changes every time the agent restarts. Store it in the agent configuration in VibeControls.',
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(['/health', '/api/agent-identity', '/api/agent-api-key']);
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 = ['/terminal/'];
131
- app.addHook('onRequest', async (request, reply) => {
132
- const url = request.url.split('?')[0]; // strip query string for matching
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['x-agent-api-key'];
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: 'Unauthorized',
147
- message: 'Invalid or missing API key. Provide the agent API key via x-agent-api-key header.',
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: '/api/tmux' });
153
- app.register(sshRoutes, { prefix: '/api/ssh' });
154
- app.register(portForwardRoutes, { prefix: '/api/port-forward' });
155
- app.register(taskRoutes, { prefix: '/api/tasks' });
156
- app.register(configRoutes, { prefix: '/api/config' });
157
- app.register(gitRoutes, { prefix: '/api/git' });
158
- app.register(fileRoutes, { prefix: '/api/files' });
159
- app.register(bookmarkRoutes, { prefix: '/api/bookmarks' });
160
- app.register(notificationRoutes, { prefix: '/api/notifications' });
161
- app.register(projectRoutes, { prefix: '/api/projects' });
162
- app.register(tunnelRoutes, { prefix: '/api/tunnel' });
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 && !['host', 'connection'].includes(k.toLowerCase()))
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 !== 'GET' && request.method !== 'HEAD') {
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 (!['transfer-encoding', 'connection'].includes(key.toLowerCase())) {
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('/terminal/:sessionId', async (request, reply) => {
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.code(502).send({ error: 'Terminal not running for this session' });
213
- return proxyToTtyd(request, reply, port, '/');
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('/terminal/:sessionId/*', async (request, reply) => {
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.code(502).send({ error: 'Terminal not running for this session' });
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('tty'))
233
- return 'tty';
243
+ if (protocols.has("tty"))
244
+ return "tty";
234
245
  return false;
235
246
  },
236
247
  });
237
- terminalWss.on('connection', (clientWs, req, ttydPort) => {
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['sec-websocket-protocol'];
253
+ const clientProtocols = req.headers["sec-websocket-protocol"];
243
254
  const protocols = clientProtocols
244
- ? clientProtocols.split(',').map((p) => p.trim())
245
- : ['tty']; // Default to 'tty' if browser didn't send it
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('open', () => {
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('message', (data, isBinary) => {
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('message', (data, isBinary) => {
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('close', (code, reason) => {
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 || upstreamWs.readyState === WebSocket.CONNECTING) {
288
- upstreamWs.close(1000, 'Client disconnected');
298
+ if (upstreamWs.readyState === WebSocket.OPEN ||
299
+ upstreamWs.readyState === WebSocket.CONNECTING) {
300
+ upstreamWs.close(1000, "Client disconnected");
289
301
  }
290
302
  });
291
- upstreamWs.on('close', (code, reason) => {
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 || clientWs.readyState === WebSocket.CONNECTING) {
295
- clientWs.close(1000, 'Upstream closed');
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('error', (err) => {
300
- console.error('[TerminalProxy] Client WS error:', err.message);
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('error', (err) => {
306
- console.error('[TerminalProxy] Upstream WS error:', err.message);
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, 'Upstream error');
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('/api/agent-tunnel', async () => {
332
+ app.get("/api/agent-tunnel", async () => {
320
333
  return {
321
334
  tunnelUrl: getAgentTunnelUrl(),
322
- status: getAgentTunnelUrl() ? 'active' : 'inactive',
335
+ status: getAgentTunnelUrl() ? "active" : "inactive",
323
336
  };
324
337
  });
325
- app.post('/api/agent-tunnel/start', async (_request, reply) => {
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: 'active' };
342
+ return { tunnelUrl: url, status: "active" };
330
343
  }
331
344
  catch (err) {
332
345
  return reply.code(500).send({
333
- error: 'Failed to start agent tunnel',
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('/api/agent-tunnel/stop', async () => {
351
+ app.post("/api/agent-tunnel/stop", async () => {
339
352
  stopAgentTunnel();
340
- return { status: 'inactive' };
353
+ return { status: "inactive" };
341
354
  });
342
355
  // Setup/bootstrap endpoint - check and install dependencies
343
- app.get('/api/setup/check', async () => {
356
+ app.get("/api/setup/check", async () => {
344
357
  return { dependencies: checkDependencies() };
345
358
  });
346
- app.post('/api/setup/install', async () => {
359
+ app.post("/api/setup/install", async () => {
347
360
  const results = await bootstrap({ verbose: false });
348
- const failed = results.filter(r => r.status === 'failed');
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
- ? 'All dependencies installed successfully'
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('/api/version', async (_request, _reply) => {
359
- const packageJson = await import('../package.json', { assert: { type: 'json' } });
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: 'v1'
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('connection', (socket) => {
378
- console.log('Client connected:', socket.id);
379
- socket.on('subscribe', (channel) => {
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('unsubscribe', (channel) => {
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('disconnect', () => {
388
- console.log('Client disconnected:', socket.id);
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('io', io);
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('onReady', async () => {
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('upgrade').slice();
419
+ const existingUpgradeListeners = app.server.listeners("upgrade").slice();
405
420
  // Remove them all
406
- app.server.removeAllListeners('upgrade');
421
+ app.server.removeAllListeners("upgrade");
407
422
  // Add a single master upgrade handler
408
- app.server.on('upgrade', (req, socket, head) => {
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('HTTP/1.1 502 Bad Gateway\r\n\r\n');
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('connection', ws, req, port);
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('[App] Terminal WS upgrade handler installed (master handler replaces Socket.IO)');
447
+ console.log("[App] Terminal WS upgrade handler installed (master handler replaces Socket.IO)");
433
448
  }
434
449
  else {
435
- console.warn('[App] Terminal proxy not initialized — WS upgrade handler not installed');
450
+ console.warn("[App] Terminal proxy not initialized — WS upgrade handler not installed");
436
451
  }
437
452
  });
438
453
  // Graceful shutdown
439
- app.addHook('onClose', async () => {
454
+ app.addHook("onClose", async () => {
440
455
  db.close();
441
456
  io.close();
442
457
  });