@agent-link/server 0.1.0

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/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,125 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { spawn } from 'child_process';
4
+ import { openSync } from 'fs';
5
+ import { join, dirname, resolve } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ import { loadServerRuntimeState, clearServerRuntimeState, getLogDir, killProcess, isProcessAlive, } from './config.js';
8
+ const program = new Command();
9
+ program
10
+ .name('agentlink-server')
11
+ .description('AgentLink relay server')
12
+ .version('0.1.0');
13
+ program
14
+ .command('start')
15
+ .description('Start the relay server')
16
+ .option('-p, --port <port>', 'Server port', '3456')
17
+ .option('-D, --daemon', 'Run server in the background as a daemon')
18
+ .action(async (options) => {
19
+ const existing = loadServerRuntimeState();
20
+ if (existing && isProcessAlive(existing.pid)) {
21
+ console.log(`Server is already running (PID ${existing.pid}, port ${existing.port}).`);
22
+ console.log('Use "agentlink-server stop" to stop it first.');
23
+ process.exit(1);
24
+ }
25
+ if (existing)
26
+ clearServerRuntimeState();
27
+ if (!options.daemon) {
28
+ // Foreground mode: run server directly
29
+ process.env.PORT = options.port;
30
+ await import('./index.js');
31
+ return;
32
+ }
33
+ // Daemon mode: spawn detached child
34
+ const __filename = fileURLToPath(import.meta.url);
35
+ const serverScript = resolve(dirname(__filename), 'index.js');
36
+ const logDir = getLogDir();
37
+ const logFile = join(logDir, 'server.log');
38
+ const errFile = join(logDir, 'server.err');
39
+ const out = openSync(logFile, 'a');
40
+ const err = openSync(errFile, 'a');
41
+ const child = spawn(process.execPath, [serverScript], {
42
+ detached: true,
43
+ stdio: ['ignore', out, err],
44
+ env: { ...process.env, PORT: options.port },
45
+ });
46
+ child.unref();
47
+ // Wait for server to write its runtime state
48
+ const maxWait = 5000;
49
+ const interval = 300;
50
+ let waited = 0;
51
+ let state = null;
52
+ while (waited < maxWait) {
53
+ await new Promise(r => setTimeout(r, interval));
54
+ waited += interval;
55
+ state = loadServerRuntimeState();
56
+ if (state && state.pid !== process.pid)
57
+ break;
58
+ }
59
+ if (state && state.pid !== process.pid) {
60
+ console.log(`Server started (PID ${state.pid}, port ${state.port}).`);
61
+ console.log(` URL: http://localhost:${state.port}`);
62
+ console.log(` Log: ${logFile}`);
63
+ }
64
+ else {
65
+ console.error('Server may have failed to start. Check logs:');
66
+ console.error(` ${errFile}`);
67
+ process.exit(1);
68
+ }
69
+ });
70
+ program
71
+ .command('stop')
72
+ .description('Stop the running relay server')
73
+ .action(async () => {
74
+ const state = loadServerRuntimeState();
75
+ if (!state) {
76
+ console.log('Server is not running (no runtime state found).');
77
+ return;
78
+ }
79
+ if (!isProcessAlive(state.pid)) {
80
+ console.log('Server is not running (process already exited).');
81
+ clearServerRuntimeState();
82
+ return;
83
+ }
84
+ console.log(`Stopping server (PID ${state.pid})...`);
85
+ if (!killProcess(state.pid)) {
86
+ console.error('Failed to stop server.');
87
+ process.exit(1);
88
+ }
89
+ // Wait for exit
90
+ const maxWait = 5000;
91
+ const interval = 200;
92
+ let waited = 0;
93
+ while (waited < maxWait) {
94
+ await new Promise(r => setTimeout(r, interval));
95
+ waited += interval;
96
+ if (!isProcessAlive(state.pid)) {
97
+ clearServerRuntimeState();
98
+ console.log('Server stopped.');
99
+ return;
100
+ }
101
+ }
102
+ clearServerRuntimeState();
103
+ console.log('Server stopped.');
104
+ });
105
+ program
106
+ .command('status')
107
+ .description('Show server status')
108
+ .action(() => {
109
+ const state = loadServerRuntimeState();
110
+ if (!state) {
111
+ console.log('Server: not running');
112
+ }
113
+ else if (!isProcessAlive(state.pid)) {
114
+ console.log('Server: not running (stale state)');
115
+ clearServerRuntimeState();
116
+ }
117
+ else {
118
+ console.log('Server: running');
119
+ console.log(` PID: ${state.pid}`);
120
+ console.log(` Port: ${state.port}`);
121
+ console.log(` Started: ${state.startedAt}`);
122
+ }
123
+ });
124
+ program.parse();
125
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ export declare const CONFIG_DIR: string;
2
+ export interface ServerRuntimeState {
3
+ pid: number;
4
+ port: number;
5
+ startedAt: string;
6
+ }
7
+ export declare function saveServerRuntimeState(state: ServerRuntimeState): void;
8
+ export declare function loadServerRuntimeState(): ServerRuntimeState | null;
9
+ export declare function clearServerRuntimeState(): void;
10
+ export declare function getLogDir(): string;
11
+ export declare function killProcess(pid: number): boolean;
12
+ export declare function isProcessAlive(pid: number): boolean;
package/dist/config.js ADDED
@@ -0,0 +1,64 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from 'fs';
2
+ import { execSync } from 'child_process';
3
+ import { join } from 'path';
4
+ import { homedir } from 'os';
5
+ export const CONFIG_DIR = join(homedir(), '.agentlink');
6
+ const SERVER_RUNTIME_FILE = join(CONFIG_DIR, 'server.json');
7
+ const LOG_DIR = join(CONFIG_DIR, 'logs');
8
+ function ensureConfigDir() {
9
+ if (!existsSync(CONFIG_DIR)) {
10
+ mkdirSync(CONFIG_DIR, { recursive: true });
11
+ }
12
+ }
13
+ export function saveServerRuntimeState(state) {
14
+ ensureConfigDir();
15
+ writeFileSync(SERVER_RUNTIME_FILE, JSON.stringify(state, null, 2) + '\n', 'utf-8');
16
+ }
17
+ export function loadServerRuntimeState() {
18
+ try {
19
+ const raw = readFileSync(SERVER_RUNTIME_FILE, 'utf-8');
20
+ return JSON.parse(raw);
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ }
26
+ export function clearServerRuntimeState() {
27
+ try {
28
+ unlinkSync(SERVER_RUNTIME_FILE);
29
+ }
30
+ catch {
31
+ // file may not exist
32
+ }
33
+ }
34
+ export function getLogDir() {
35
+ if (!existsSync(LOG_DIR)) {
36
+ mkdirSync(LOG_DIR, { recursive: true });
37
+ }
38
+ return LOG_DIR;
39
+ }
40
+ // ── Cross-platform process management ──
41
+ export function killProcess(pid) {
42
+ try {
43
+ if (process.platform === 'win32') {
44
+ execSync(`taskkill /pid ${pid} /f /t`, { stdio: 'ignore' });
45
+ }
46
+ else {
47
+ process.kill(pid, 'SIGTERM');
48
+ }
49
+ return true;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ export function isProcessAlive(pid) {
56
+ try {
57
+ process.kill(pid, 0);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
64
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AACpF,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAE7B,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AACxD,MAAM,mBAAmB,GAAG,IAAI,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AAC5D,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAEzC,SAAS,eAAe;IACtB,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;AACH,CAAC;AAUD,MAAM,UAAU,sBAAsB,CAAC,KAAyB;IAC9D,eAAe,EAAE,CAAC;IAClB,aAAa,CAAC,mBAAmB,EAAE,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,OAAO,CAAC,CAAC;AACrF,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,mBAAmB,EAAE,OAAO,CAAC,CAAC;QACvD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAuB,CAAC;IAC/C,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB;IACrC,IAAI,CAAC;QACH,UAAU,CAAC,mBAAmB,CAAC,CAAC;IAClC,CAAC;IAAC,MAAM,CAAC;QACP,qBAAqB;IACvB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,EAAE,CAAC;QACzB,SAAS,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,0CAA0C;AAE1C,MAAM,UAAU,WAAW,CAAC,GAAW;IACrC,IAAI,CAAC;QACH,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO,EAAE,CAAC;YACjC,QAAQ,CAAC,iBAAiB,GAAG,QAAQ,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC9D,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;QAC/B,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACrB,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC"}
@@ -0,0 +1,27 @@
1
+ import { WebSocket } from 'ws';
2
+ export interface AgentSession {
3
+ ws: WebSocket;
4
+ agentId: string;
5
+ name: string;
6
+ hostname: string;
7
+ workDir: string;
8
+ sessionId: string;
9
+ sessionKey: Uint8Array | null;
10
+ connectedAt: Date;
11
+ isAlive: boolean;
12
+ }
13
+ export interface WebClient {
14
+ ws: WebSocket;
15
+ clientId: string;
16
+ sessionId: string;
17
+ sessionKey: Uint8Array | null;
18
+ connectedAt: Date;
19
+ isAlive: boolean;
20
+ }
21
+ export declare const agents: Map<string, AgentSession>;
22
+ export declare const sessionToAgent: Map<string, string>;
23
+ export declare const webClients: Map<string, WebClient>;
24
+ /**
25
+ * Generate a short, URL-safe session ID
26
+ */
27
+ export declare function generateSessionId(): string;
@@ -0,0 +1,14 @@
1
+ import { randomBytes } from 'crypto';
2
+ // Agent sessions: agentId → AgentSession
3
+ export const agents = new Map();
4
+ // Session ID → agentId (reverse lookup)
5
+ export const sessionToAgent = new Map();
6
+ // Web clients: clientId → WebClient
7
+ export const webClients = new Map();
8
+ /**
9
+ * Generate a short, URL-safe session ID
10
+ */
11
+ export function generateSessionId() {
12
+ return randomBytes(12).toString('base64url');
13
+ }
14
+ //# sourceMappingURL=context.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAuBrC,yCAAyC;AACzC,MAAM,CAAC,MAAM,MAAM,GAAG,IAAI,GAAG,EAAwB,CAAC;AAEtD,wCAAwC;AACxC,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;AAExD,oCAAoC;AACpC,MAAM,CAAC,MAAM,UAAU,GAAG,IAAI,GAAG,EAAqB,CAAC;AAEvD;;GAEG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
@@ -0,0 +1,23 @@
1
+ export declare function generateSessionKey(): Uint8Array;
2
+ export declare function encrypt(data: unknown, key: Uint8Array): Promise<{
3
+ n: string;
4
+ c: string;
5
+ z?: boolean;
6
+ }>;
7
+ export declare function decrypt(encrypted: {
8
+ n: string;
9
+ c: string;
10
+ z?: boolean;
11
+ }, key: Uint8Array): Promise<unknown | null>;
12
+ export declare function isEncrypted(msg: unknown): msg is {
13
+ n: string;
14
+ c: string;
15
+ z?: boolean;
16
+ };
17
+ export declare function encodeKey(key: Uint8Array): string;
18
+ export declare function decodeKey(encodedKey: string): Uint8Array;
19
+ export declare function parseMessage(data: string, sessionKey: Uint8Array | null): Promise<Record<string, unknown> | null>;
20
+ export declare function encryptAndSend(ws: {
21
+ send: (data: string) => void;
22
+ readyState: number;
23
+ }, msg: unknown, sessionKey: Uint8Array | null): Promise<void>;
@@ -0,0 +1,83 @@
1
+ import tweetnacl from 'tweetnacl';
2
+ import tweetnaclUtil from 'tweetnacl-util';
3
+ import zlib from 'zlib';
4
+ import { promisify } from 'util';
5
+ const gzip = promisify(zlib.gzip);
6
+ const gunzip = promisify(zlib.gunzip);
7
+ const { encodeBase64, decodeBase64, encodeUTF8, decodeUTF8 } = tweetnaclUtil;
8
+ const COMPRESS_THRESHOLD = 512;
9
+ export function generateSessionKey() {
10
+ return tweetnacl.randomBytes(32);
11
+ }
12
+ export async function encrypt(data, key) {
13
+ const nonce = tweetnacl.randomBytes(24);
14
+ const jsonStr = JSON.stringify(data);
15
+ let message;
16
+ let compressed = false;
17
+ if (jsonStr.length > COMPRESS_THRESHOLD) {
18
+ const compressedBuf = await gzip(Buffer.from(jsonStr, 'utf8'));
19
+ message = new Uint8Array(compressedBuf);
20
+ compressed = true;
21
+ }
22
+ else {
23
+ message = decodeUTF8(jsonStr);
24
+ }
25
+ const encrypted = tweetnacl.secretbox(message, nonce, key);
26
+ const result = {
27
+ n: encodeBase64(nonce),
28
+ c: encodeBase64(encrypted),
29
+ };
30
+ if (compressed)
31
+ result.z = true;
32
+ return result;
33
+ }
34
+ export async function decrypt(encrypted, key) {
35
+ try {
36
+ const nonce = decodeBase64(encrypted.n);
37
+ const ciphertext = decodeBase64(encrypted.c);
38
+ const decrypted = tweetnacl.secretbox.open(ciphertext, nonce, key);
39
+ if (!decrypted)
40
+ return null;
41
+ if (encrypted.z) {
42
+ const decompressed = await gunzip(Buffer.from(decrypted));
43
+ return JSON.parse(decompressed.toString('utf8'));
44
+ }
45
+ else {
46
+ return JSON.parse(encodeUTF8(decrypted));
47
+ }
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ export function isEncrypted(msg) {
54
+ return msg !== null && typeof msg === 'object' && typeof msg.n === 'string' && typeof msg.c === 'string';
55
+ }
56
+ export function encodeKey(key) {
57
+ return encodeBase64(key);
58
+ }
59
+ export function decodeKey(encodedKey) {
60
+ return decodeBase64(encodedKey);
61
+ }
62
+ export async function parseMessage(data, sessionKey) {
63
+ try {
64
+ const parsed = JSON.parse(data);
65
+ if (sessionKey && isEncrypted(parsed)) {
66
+ return await decrypt(parsed, sessionKey);
67
+ }
68
+ return parsed;
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ export async function encryptAndSend(ws, msg, sessionKey) {
75
+ if (sessionKey) {
76
+ const encrypted = await encrypt(msg, sessionKey);
77
+ ws.send(JSON.stringify(encrypted));
78
+ }
79
+ else {
80
+ ws.send(JSON.stringify(msg));
81
+ }
82
+ }
83
+ //# sourceMappingURL=encryption.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encryption.js","sourceRoot":"","sources":["../src/encryption.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,99 @@
1
+ import express from 'express';
2
+ import { createServer } from 'http';
3
+ import { WebSocketServer } from 'ws';
4
+ import { fileURLToPath } from 'url';
5
+ import { dirname, join } from 'path';
6
+ import { agents, webClients } from './context.js';
7
+ import { handleAgentConnection } from './ws-agent.js';
8
+ import { handleWebConnection } from './ws-client.js';
9
+ import { saveServerRuntimeState, clearServerRuntimeState } from './config.js';
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ const PORT = parseInt(process.env.PORT || '3456', 10);
12
+ const app = express();
13
+ const server = createServer(app);
14
+ const wss = new WebSocketServer({ server });
15
+ const webDir = join(__dirname, '../web');
16
+ // Serve static assets from web/
17
+ app.use(express.static(webDir));
18
+ // SPA fallback: /s/:sessionId → serve index.html (Vue router handles the rest)
19
+ app.get('/s/:sessionId', (_req, res) => {
20
+ res.sendFile(join(webDir, 'index.html'));
21
+ });
22
+ // Health check
23
+ app.get('/api/health', (_req, res) => {
24
+ res.json({
25
+ status: 'ok',
26
+ agents: agents.size,
27
+ webClients: webClients.size,
28
+ timestamp: new Date().toISOString(),
29
+ });
30
+ });
31
+ // Session info API (web client fetches this to know agent details)
32
+ app.get('/api/session/:sessionId', (req, res) => {
33
+ const { sessionId } = req.params;
34
+ for (const [, agent] of agents) {
35
+ if (agent.sessionId === sessionId) {
36
+ res.json({
37
+ sessionId,
38
+ agent: {
39
+ name: agent.name,
40
+ workDir: agent.workDir,
41
+ connectedAt: agent.connectedAt,
42
+ },
43
+ });
44
+ return;
45
+ }
46
+ }
47
+ res.status(404).json({ error: 'Session not found' });
48
+ });
49
+ // WebSocket routing: agent or web client
50
+ wss.on('connection', (ws, req) => {
51
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
52
+ const type = url.searchParams.get('type');
53
+ if (type === 'agent') {
54
+ handleAgentConnection(ws, req);
55
+ }
56
+ else if (type === 'web') {
57
+ handleWebConnection(ws, req);
58
+ }
59
+ else {
60
+ ws.send(JSON.stringify({ type: 'error', message: 'Unknown type. Use ?type=agent or ?type=web' }));
61
+ ws.close();
62
+ }
63
+ });
64
+ // Heartbeat every 30s to detect dead connections
65
+ setInterval(() => {
66
+ for (const [agentId, agent] of agents) {
67
+ if (!agent.isAlive) {
68
+ console.log(`[Heartbeat] Agent ${agent.name} timed out`);
69
+ agent.ws.terminate();
70
+ return;
71
+ }
72
+ agent.isAlive = false;
73
+ agent.ws.ping();
74
+ }
75
+ for (const [clientId, client] of webClients) {
76
+ if (!client.isAlive) {
77
+ client.ws.terminate();
78
+ webClients.delete(clientId);
79
+ return;
80
+ }
81
+ client.isAlive = false;
82
+ client.ws.ping();
83
+ }
84
+ }, 30_000);
85
+ server.listen(PORT, () => {
86
+ console.log(`[AgentLink] Server listening on http://localhost:${PORT}`);
87
+ console.log(`[AgentLink] Web UI: ${webDir}`);
88
+ // Write server runtime state for CLI stop command
89
+ saveServerRuntimeState({
90
+ pid: process.pid,
91
+ port: PORT,
92
+ startedAt: new Date().toISOString(),
93
+ });
94
+ });
95
+ // Clean up runtime state on exit
96
+ process.on('exit', clearServerRuntimeState);
97
+ process.on('SIGINT', () => { clearServerRuntimeState(); process.exit(0); });
98
+ process.on('SIGTERM', () => { clearServerRuntimeState(); process.exit(0); });
99
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { WebSocket } from 'ws';
2
+ import { IncomingMessage } from 'http';
3
+ export declare function handleAgentConnection(ws: WebSocket, req: IncomingMessage): void;
@@ -0,0 +1,74 @@
1
+ import { WebSocket } from 'ws';
2
+ import { randomUUID } from 'crypto';
3
+ import { agents, sessionToAgent, webClients, generateSessionId, } from './context.js';
4
+ import { generateSessionKey, encodeKey, parseMessage, encryptAndSend } from './encryption.js';
5
+ export function handleAgentConnection(ws, req) {
6
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
7
+ const agentId = url.searchParams.get('id') || randomUUID();
8
+ const name = url.searchParams.get('name') || `Agent-${agentId.slice(0, 8)}`;
9
+ const workDir = url.searchParams.get('workDir') || 'unknown';
10
+ const hostname = url.searchParams.get('hostname') || '';
11
+ const sessionId = generateSessionId();
12
+ const sessionKey = generateSessionKey();
13
+ const agent = {
14
+ ws,
15
+ agentId,
16
+ name,
17
+ hostname,
18
+ workDir,
19
+ sessionId,
20
+ sessionKey,
21
+ connectedAt: new Date(),
22
+ isAlive: true,
23
+ };
24
+ agents.set(agentId, agent);
25
+ sessionToAgent.set(sessionId, agentId);
26
+ console.log(`[Agent] Registered: ${name} (${agentId}), session: ${sessionId}`);
27
+ // Send registration with session key (this initial message is plain text)
28
+ ws.send(JSON.stringify({
29
+ type: 'registered',
30
+ agentId,
31
+ sessionId,
32
+ sessionKey: encodeKey(sessionKey),
33
+ }));
34
+ ws.on('message', (data) => {
35
+ handleAgentMessage(agentId, data.toString());
36
+ });
37
+ ws.on('close', () => {
38
+ console.log(`[Agent] Disconnected: ${name} (${agentId})`);
39
+ sessionToAgent.delete(sessionId);
40
+ agents.delete(agentId);
41
+ // Notify connected web clients that agent is gone
42
+ for (const [, client] of webClients) {
43
+ if (client.sessionId === sessionId && client.ws.readyState === WebSocket.OPEN) {
44
+ encryptAndSend(client.ws, { type: 'agent_disconnected' }, client.sessionKey);
45
+ }
46
+ }
47
+ });
48
+ ws.on('pong', () => {
49
+ agent.isAlive = true;
50
+ });
51
+ }
52
+ async function handleAgentMessage(agentId, raw) {
53
+ const agent = agents.get(agentId);
54
+ if (!agent)
55
+ return;
56
+ const msg = await parseMessage(raw, agent.sessionKey);
57
+ if (!msg) {
58
+ console.error(`[Agent] Failed to parse/decrypt message from ${agentId}`);
59
+ return;
60
+ }
61
+ // Intercept workdir_changed to keep server state in sync
62
+ if (msg.type === 'workdir_changed' && typeof msg.workDir === 'string') {
63
+ agent.workDir = msg.workDir;
64
+ console.log(`[Agent] ${agent.name} changed workDir to: ${msg.workDir}`);
65
+ }
66
+ // Forward agent messages to all web clients connected to this session
67
+ // Re-encrypt with each client's own session key
68
+ for (const [, client] of webClients) {
69
+ if (client.sessionId === agent.sessionId && client.ws.readyState === WebSocket.OPEN) {
70
+ encryptAndSend(client.ws, msg, client.sessionKey);
71
+ }
72
+ }
73
+ }
74
+ //# sourceMappingURL=ws-agent.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-agent.js","sourceRoot":"","sources":["../src/ws-agent.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import { WebSocket } from 'ws';
2
+ import { IncomingMessage } from 'http';
3
+ export declare function handleWebConnection(ws: WebSocket, req: IncomingMessage): void;
@@ -0,0 +1,70 @@
1
+ import { WebSocket } from 'ws';
2
+ import { randomUUID } from 'crypto';
3
+ import { agents, sessionToAgent, webClients, } from './context.js';
4
+ import { generateSessionKey, encodeKey, parseMessage, encryptAndSend } from './encryption.js';
5
+ export function handleWebConnection(ws, req) {
6
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
7
+ const sessionId = url.searchParams.get('sessionId');
8
+ const clientId = randomUUID();
9
+ if (!sessionId) {
10
+ ws.send(JSON.stringify({ type: 'error', message: 'Missing sessionId' }));
11
+ ws.close();
12
+ return;
13
+ }
14
+ // Check if agent exists for this session
15
+ const agentId = sessionToAgent.get(sessionId);
16
+ const agent = agentId ? agents.get(agentId) : undefined;
17
+ const sessionKey = generateSessionKey();
18
+ const client = {
19
+ ws,
20
+ clientId,
21
+ sessionId,
22
+ sessionKey,
23
+ connectedAt: new Date(),
24
+ isAlive: true,
25
+ };
26
+ webClients.set(clientId, client);
27
+ // Send connection result with agent info and session key (plain text — key exchange)
28
+ ws.send(JSON.stringify({
29
+ type: 'connected',
30
+ clientId,
31
+ sessionKey: encodeKey(sessionKey),
32
+ agent: agent ? {
33
+ agentId: agent.agentId,
34
+ name: agent.name,
35
+ hostname: agent.hostname,
36
+ workDir: agent.workDir,
37
+ } : null,
38
+ }));
39
+ console.log(`[Web] Client ${clientId.slice(0, 8)} connected to session ${sessionId}, agent: ${agent ? agent.name : 'none'}`);
40
+ ws.on('message', (data) => {
41
+ handleWebMessage(clientId, data.toString());
42
+ });
43
+ ws.on('close', () => {
44
+ console.log(`[Web] Client ${clientId.slice(0, 8)} disconnected`);
45
+ webClients.delete(clientId);
46
+ });
47
+ ws.on('pong', () => {
48
+ client.isAlive = true;
49
+ });
50
+ }
51
+ async function handleWebMessage(clientId, raw) {
52
+ const client = webClients.get(clientId);
53
+ if (!client)
54
+ return;
55
+ const msg = await parseMessage(raw, client.sessionKey);
56
+ if (!msg) {
57
+ console.error(`[Web] Failed to parse/decrypt message from ${clientId.slice(0, 8)}`);
58
+ return;
59
+ }
60
+ // Find the agent for this session and forward
61
+ const agentId = sessionToAgent.get(client.sessionId);
62
+ const agent = agentId ? agents.get(agentId) : undefined;
63
+ if (!agent || agent.ws.readyState !== WebSocket.OPEN) {
64
+ encryptAndSend(client.ws, { type: 'error', message: 'Agent not connected' }, client.sessionKey);
65
+ return;
66
+ }
67
+ // Forward web client message to agent, encrypted with agent's session key
68
+ encryptAndSend(agent.ws, msg, agent.sessionKey);
69
+ }
70
+ //# sourceMappingURL=ws-client.js.map