@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.
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 +144 -130
  5. package/dist/app.js.map +1 -1
  6. package/dist/cli.d.ts +1 -1
  7. package/dist/cli.js +342 -332
  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,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 '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,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('/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
+ ]);
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 = ['/terminal/'];
131
- app.addHook('onRequest', async (request, reply) => {
132
- const url = request.url.split('?')[0]; // strip query string for matching
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['x-agent-api-key'];
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: 'Unauthorized',
147
- message: 'Invalid or missing API key. Provide the agent API key via x-agent-api-key header.',
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: '/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' });
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 && !['host', 'connection'].includes(k.toLowerCase()))
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 !== 'GET' && request.method !== 'HEAD') {
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 (!['transfer-encoding', 'connection'].includes(key.toLowerCase())) {
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('/terminal/:sessionId', async (request, reply) => {
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.code(502).send({ error: 'Terminal not running for this session' });
213
- return proxyToTtyd(request, reply, port, '/');
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('/terminal/:sessionId/*', async (request, reply) => {
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.code(502).send({ error: 'Terminal not running for this session' });
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('tty'))
233
- return 'tty';
242
+ if (protocols.has("tty"))
243
+ return "tty";
234
244
  return false;
235
245
  },
236
246
  });
237
- terminalWss.on('connection', (clientWs, req, ttydPort) => {
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['sec-websocket-protocol'];
252
+ const clientProtocols = req.headers["sec-websocket-protocol"];
243
253
  const protocols = clientProtocols
244
- ? clientProtocols.split(',').map((p) => p.trim())
245
- : ['tty']; // Default to 'tty' if browser didn't send it
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('open', () => {
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('message', (data, isBinary) => {
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('message', (data, isBinary) => {
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('close', (code, reason) => {
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 || upstreamWs.readyState === WebSocket.CONNECTING) {
288
- upstreamWs.close(1000, 'Client disconnected');
297
+ if (upstreamWs.readyState === WebSocket.OPEN ||
298
+ upstreamWs.readyState === WebSocket.CONNECTING) {
299
+ upstreamWs.close(1000, "Client disconnected");
289
300
  }
290
301
  });
291
- upstreamWs.on('close', (code, reason) => {
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 || clientWs.readyState === WebSocket.CONNECTING) {
295
- clientWs.close(1000, 'Upstream closed');
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('error', (err) => {
300
- console.error('[TerminalProxy] Client WS error:', err.message);
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('error', (err) => {
306
- console.error('[TerminalProxy] Upstream WS error:', err.message);
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, 'Upstream error');
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('/api/agent-tunnel', async () => {
331
+ app.get("/api/agent-tunnel", async () => {
320
332
  return {
321
333
  tunnelUrl: getAgentTunnelUrl(),
322
- status: getAgentTunnelUrl() ? 'active' : 'inactive',
334
+ status: getAgentTunnelUrl() ? "active" : "inactive",
323
335
  };
324
336
  });
325
- app.post('/api/agent-tunnel/start', async (_request, reply) => {
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: 'active' };
341
+ return { tunnelUrl: url, status: "active" };
330
342
  }
331
343
  catch (err) {
332
344
  return reply.code(500).send({
333
- error: 'Failed to start agent tunnel',
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('/api/agent-tunnel/stop', async () => {
350
+ app.post("/api/agent-tunnel/stop", async () => {
339
351
  stopAgentTunnel();
340
- return { status: 'inactive' };
352
+ return { status: "inactive" };
341
353
  });
342
354
  // Setup/bootstrap endpoint - check and install dependencies
343
- app.get('/api/setup/check', async () => {
355
+ app.get("/api/setup/check", async () => {
344
356
  return { dependencies: checkDependencies() };
345
357
  });
346
- app.post('/api/setup/install', async () => {
358
+ app.post("/api/setup/install", async () => {
347
359
  const results = await bootstrap({ verbose: false });
348
- const failed = results.filter(r => r.status === 'failed');
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
- ? 'All dependencies installed successfully'
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('/api/version', async (_request, _reply) => {
359
- const packageJson = await import('../package.json', { assert: { type: 'json' } });
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: 'v1'
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('connection', (socket) => {
378
- console.log('Client connected:', socket.id);
379
- socket.on('subscribe', (channel) => {
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('unsubscribe', (channel) => {
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('disconnect', () => {
388
- console.log('Client disconnected:', socket.id);
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('io', io);
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('onReady', async () => {
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('upgrade').slice();
418
+ const existingUpgradeListeners = app.server.listeners("upgrade").slice();
405
419
  // Remove them all
406
- app.server.removeAllListeners('upgrade');
420
+ app.server.removeAllListeners("upgrade");
407
421
  // Add a single master upgrade handler
408
- app.server.on('upgrade', (req, socket, head) => {
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('HTTP/1.1 502 Bad Gateway\r\n\r\n');
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('connection', ws, req, port);
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('[App] Terminal WS upgrade handler installed (master handler replaces Socket.IO)');
446
+ console.log("[App] Terminal WS upgrade handler installed (master handler replaces Socket.IO)");
433
447
  }
434
448
  else {
435
- console.warn('[App] Terminal proxy not initialized — WS upgrade handler not installed');
449
+ console.warn("[App] Terminal proxy not initialized — WS upgrade handler not installed");
436
450
  }
437
451
  });
438
452
  // Graceful shutdown
439
- app.addHook('onClose', async () => {
453
+ app.addHook("onClose", async () => {
440
454
  db.close();
441
455
  io.close();
442
456
  });