@messenger-agent/messenger-agent 0.24.0-alpha.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/dist/app.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import { Hono } from "hono";
2
+ import { type MessengerAgentConfig } from "./config.js";
3
+ export declare function createApp(config?: MessengerAgentConfig): Hono;
4
+ declare const _default: Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
5
+ export default _default;
package/dist/app.js ADDED
@@ -0,0 +1,17 @@
1
+ import { Hono } from "hono";
2
+ import { logger as honoLogger } from "hono/logger";
3
+ import { agentAuthMiddleware } from "@messenger-agent/shared/agent-auth";
4
+ import { createAgentFileRoutes } from "@messenger-agent/shared/agent-files";
5
+ import { logger } from "@messenger-agent/shared/logger";
6
+ import { createAgentWorkspaceRoutes } from "@messenger-agent/shared/workspace-routes";
7
+ import { appConfig } from "./config.js";
8
+ export function createApp(config = appConfig) {
9
+ const app = new Hono();
10
+ app.use(honoLogger((message, ...rest) => logger.info(message, ...rest)));
11
+ app.get("/health", (c) => c.json({ status: "ok", role: "messenger-agent" }));
12
+ app.use("/*", agentAuthMiddleware(config.authTokens));
13
+ app.route("/v1/files", createAgentFileRoutes());
14
+ app.route("/v1/workspaces", createAgentWorkspaceRoutes({ config, textMaxBytes: config.textMaxBytes }));
15
+ return app;
16
+ }
17
+ export default createApp();
@@ -0,0 +1,20 @@
1
+ import { type AgentAuthConfig, type AgentWorkspacesConfig } from "@messenger-agent/shared/agent-config";
2
+ import { type LogLevel } from "@messenger-agent/shared/logger";
3
+ export type MessengerAgentConfig = {
4
+ configPath: string;
5
+ logLevel: LogLevel;
6
+ logFile?: string;
7
+ port: number;
8
+ textMaxBytes: number;
9
+ tunnel: {
10
+ enabled: boolean;
11
+ serverUrl?: string;
12
+ tunnelId?: string;
13
+ token?: string;
14
+ reconnectInitialMs: number;
15
+ reconnectMaxMs: number;
16
+ heartbeatIntervalMs: number;
17
+ };
18
+ } & AgentAuthConfig & AgentWorkspacesConfig;
19
+ export declare function loadMessengerAgentConfig(configPath?: string): MessengerAgentConfig;
20
+ export declare const appConfig: MessengerAgentConfig;
package/dist/config.js ADDED
@@ -0,0 +1,78 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { parse } from "yaml";
3
+ import { z } from "zod";
4
+ import { normalizeOptional, parseAgentAuthConfig, parseAgentWorkspacesConfig, } from "@messenger-agent/shared/agent-config";
5
+ import { DEFAULT_TEXT_MAX_BYTES } from "@messenger-agent/shared/workspace-files";
6
+ import { logger, normalizeLogLevel, setLogFile, setLogLevel } from "@messenger-agent/shared/logger";
7
+ const DEFAULT_TUNNEL_SERVER_URL = "wss://m.elevo.vip/agent-bridge/tunnel";
8
+ const RawConfigSchema = z
9
+ .object({
10
+ log_level: z.string().optional(),
11
+ log_file: z.string().optional(),
12
+ port: z.coerce.number().int().positive().optional(),
13
+ messenger: z
14
+ .object({
15
+ port: z.coerce.number().int().positive().optional(),
16
+ log_file: z.string().optional(),
17
+ })
18
+ .optional(),
19
+ auth_tokens: z.record(z.string(), z.string()).optional(),
20
+ workspaces: z
21
+ .array(z.object({ id: z.string().optional(), name: z.string().optional(), path: z.string().optional() }))
22
+ .optional(),
23
+ workspace_file_text_max_bytes: z.coerce.number().int().positive().optional(),
24
+ tunnel: z
25
+ .object({
26
+ enabled: z.boolean().optional(),
27
+ server_url: z.string().optional(),
28
+ tunnel_id: z.string().optional(),
29
+ token: z.string().optional(),
30
+ reconnect_initial_ms: z.number().int().positive().optional(),
31
+ reconnect_max_ms: z.number().int().positive().optional(),
32
+ heartbeat_interval_ms: z.number().int().positive().optional(),
33
+ })
34
+ .optional(),
35
+ })
36
+ .loose();
37
+ export function loadMessengerAgentConfig(configPath = process.env.MESSENGER_AGENT_CONFIG_PATH ?? process.env.AGENT_CONFIG_PATH ?? "./agent-config.yaml") {
38
+ let parsedYaml = {};
39
+ if (existsSync(configPath)) {
40
+ try {
41
+ parsedYaml = parse(readFileSync(configPath, "utf8"));
42
+ }
43
+ catch (err) {
44
+ logger.error(`Failed to parse messenger-agent config at ${configPath}:`, err);
45
+ }
46
+ }
47
+ else {
48
+ logger.warn(`Messenger-agent config file not found at ${configPath}`);
49
+ }
50
+ const parsed = RawConfigSchema.safeParse(parsedYaml ?? {});
51
+ if (!parsed.success)
52
+ logger.error(`Invalid messenger-agent config at ${configPath}:`, parsed.error.issues);
53
+ const data = parsed.success ? parsed.data : {};
54
+ const logLevel = normalizeLogLevel(data.log_level);
55
+ const logFile = normalizeOptional(data.messenger?.log_file) ?? normalizeOptional(data.log_file);
56
+ const enabled = data.tunnel?.enabled ?? false;
57
+ setLogLevel(logLevel);
58
+ setLogFile(logFile);
59
+ return {
60
+ configPath,
61
+ logLevel,
62
+ logFile,
63
+ port: data.messenger?.port ?? data.port ?? 8301,
64
+ textMaxBytes: data.workspace_file_text_max_bytes ?? DEFAULT_TEXT_MAX_BYTES,
65
+ tunnel: {
66
+ enabled,
67
+ serverUrl: normalizeOptional(data.tunnel?.server_url) ?? (enabled ? DEFAULT_TUNNEL_SERVER_URL : undefined),
68
+ tunnelId: normalizeOptional(data.tunnel?.tunnel_id),
69
+ token: normalizeOptional(data.tunnel?.token),
70
+ reconnectInitialMs: data.tunnel?.reconnect_initial_ms ?? 1000,
71
+ reconnectMaxMs: data.tunnel?.reconnect_max_ms ?? 30000,
72
+ heartbeatIntervalMs: data.tunnel?.heartbeat_interval_ms ?? 30000,
73
+ },
74
+ ...parseAgentAuthConfig(data),
75
+ ...parseAgentWorkspacesConfig(data),
76
+ };
77
+ }
78
+ export const appConfig = loadMessengerAgentConfig();
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ import { serve } from "@hono/node-server";
3
+ import app from "./app.js";
4
+ import { appConfig } from "./config.js";
5
+ import { WorkspaceTunnelClient } from "./tunnel-client.js";
6
+ import { logger } from "@messenger-agent/shared/logger";
7
+ const tunnelClient = appConfig.tunnel.enabled ? new WorkspaceTunnelClient(appConfig) : undefined;
8
+ if (tunnelClient) {
9
+ tunnelClient.start().catch((err) => logger.error("[WorkspaceTunnel] stopped unexpectedly:", err));
10
+ }
11
+ else {
12
+ serve({ fetch: app.fetch, port: appConfig.port }, () => {
13
+ logger.info(`messenger-agent listening on http://0.0.0.0:${appConfig.port}`);
14
+ });
15
+ }
16
+ const stop = () => tunnelClient?.stop();
17
+ process.once("SIGINT", stop);
18
+ process.once("SIGTERM", stop);
@@ -0,0 +1,15 @@
1
+ import { type MessengerAgentConfig } from "./config.js";
2
+ export declare class WorkspaceTunnelClient {
3
+ private readonly config;
4
+ private stopped;
5
+ private socket?;
6
+ private readonly running;
7
+ private readonly workspaceRoutes;
8
+ private readonly fileRoutes;
9
+ constructor(config?: MessengerAgentConfig);
10
+ start(): Promise<void>;
11
+ stop(): void;
12
+ private connectOnce;
13
+ private send;
14
+ private handleRequest;
15
+ }
@@ -0,0 +1,159 @@
1
+ import { createRequire } from "node:module";
2
+ import WebSocket from "ws";
3
+ import { logger } from "@messenger-agent/shared/logger";
4
+ import { createAgentFileRoutes } from "@messenger-agent/shared/agent-files";
5
+ import { createPrefixedRouteRequest, createWorkspaceRouteRequest, isAgentFileHttpTunnelRequest, isWorkspaceHttpTunnelRequest, sendHttpTunnelResponse, } from "@messenger-agent/shared/tunnel-http";
6
+ import { CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE, CODING_AGENT_TUNNEL_PROTOCOL_VERSION, } from "@messenger-agent/shared/tunnel-protocol";
7
+ import { createAgentWorkspaceRoutes } from "@messenger-agent/shared/workspace-routes";
8
+ import { appConfig } from "./config.js";
9
+ const clientVersion = createRequire(import.meta.url)("../package.json").version;
10
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
11
+ function parseServerMessage(raw) {
12
+ try {
13
+ const value = JSON.parse(raw.toString());
14
+ return value && typeof value === "object" && "type" in value ? value : undefined;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ export class WorkspaceTunnelClient {
21
+ config;
22
+ stopped = false;
23
+ socket;
24
+ running = new Map();
25
+ workspaceRoutes;
26
+ fileRoutes = createAgentFileRoutes();
27
+ constructor(config = appConfig) {
28
+ this.config = config;
29
+ this.workspaceRoutes = createAgentWorkspaceRoutes({ config, textMaxBytes: config.textMaxBytes });
30
+ }
31
+ async start() {
32
+ if (!this.config.tunnel.enabled)
33
+ return;
34
+ if (!this.config.tunnel.serverUrl || !this.config.tunnel.tunnelId || !this.config.tunnel.token) {
35
+ throw new Error("Workspace tunnel requires server_url, tunnel_id, and token when enabled");
36
+ }
37
+ let backoffMs = this.config.tunnel.reconnectInitialMs;
38
+ while (!this.stopped) {
39
+ try {
40
+ await this.connectOnce();
41
+ backoffMs = this.config.tunnel.reconnectInitialMs;
42
+ }
43
+ catch (err) {
44
+ if (!this.stopped)
45
+ logger.warn("[WorkspaceTunnel] connection failed:", err);
46
+ }
47
+ if (!this.stopped) {
48
+ await delay(backoffMs);
49
+ backoffMs = Math.min(backoffMs * 2, this.config.tunnel.reconnectMaxMs);
50
+ }
51
+ }
52
+ }
53
+ stop() {
54
+ this.stopped = true;
55
+ for (const controller of this.running.values())
56
+ controller.abort();
57
+ this.running.clear();
58
+ this.socket?.close();
59
+ }
60
+ connectOnce() {
61
+ return new Promise((resolve, reject) => {
62
+ const ws = new WebSocket(this.config.tunnel.serverUrl, {
63
+ headers: { Authorization: `Bearer ${this.config.tunnel.token}` },
64
+ });
65
+ this.socket = ws;
66
+ let heartbeatTimer;
67
+ let settled = false;
68
+ const cleanup = () => {
69
+ if (heartbeatTimer)
70
+ clearInterval(heartbeatTimer);
71
+ if (this.socket === ws)
72
+ this.socket = undefined;
73
+ for (const controller of this.running.values())
74
+ controller.abort(new Error("Workspace tunnel closed"));
75
+ this.running.clear();
76
+ };
77
+ ws.on("open", () => {
78
+ this.send({
79
+ type: "hello",
80
+ protocolVersion: CODING_AGENT_TUNNEL_PROTOCOL_VERSION,
81
+ tunnelId: this.config.tunnel.tunnelId,
82
+ agentType: "workspace",
83
+ capabilities: ["workspace.files", "workspace.git", "workspace.tasks"],
84
+ clientVersion,
85
+ workspaces: [...this.config.workspaces.values()]
86
+ .map(({ id, name }) => ({ id, name }))
87
+ .sort((left, right) => left.id.localeCompare(right.id)),
88
+ });
89
+ heartbeatTimer = setInterval(() => this.send({ type: "heartbeat" }), this.config.tunnel.heartbeatIntervalMs);
90
+ logger.info(`[WorkspaceTunnel] connected to ${this.config.tunnel.serverUrl} as ${this.config.tunnel.tunnelId}`);
91
+ });
92
+ ws.on("message", (raw) => {
93
+ const message = parseServerMessage(raw);
94
+ if (!message || message.type === "heartbeat")
95
+ return;
96
+ if (message.type === "cancel") {
97
+ this.running.get(message.id)?.abort();
98
+ }
99
+ else if (message.type === "request") {
100
+ void this.handleRequest(message);
101
+ }
102
+ });
103
+ ws.once("error", (err) => {
104
+ if (!settled) {
105
+ settled = true;
106
+ cleanup();
107
+ reject(err);
108
+ }
109
+ });
110
+ ws.once("close", (code, reason) => {
111
+ cleanup();
112
+ if (code === CODING_AGENT_TUNNEL_DUPLICATE_CLOSE_CODE || code === 1008) {
113
+ this.stopped = true;
114
+ logger.warn(`[WorkspaceTunnel] connection rejected with code ${code}; reconnect disabled until service restart: ${reason.toString()}`);
115
+ }
116
+ if (!settled) {
117
+ settled = true;
118
+ resolve();
119
+ }
120
+ });
121
+ });
122
+ }
123
+ send(message) {
124
+ if (this.socket?.readyState === WebSocket.OPEN)
125
+ this.socket.send(JSON.stringify(message));
126
+ }
127
+ async handleRequest(message) {
128
+ const isFileRequest = isAgentFileHttpTunnelRequest(message);
129
+ if (!isFileRequest && !isWorkspaceHttpTunnelRequest(message)) {
130
+ this.send({
131
+ type: "response.error",
132
+ id: message.id,
133
+ code: "unsupported_request",
134
+ errorText: `Unsupported workspace tunnel request: ${message.method} ${message.path}`,
135
+ });
136
+ return;
137
+ }
138
+ const controller = new AbortController();
139
+ this.running.set(message.id, controller);
140
+ try {
141
+ const request = isFileRequest
142
+ ? createPrefixedRouteRequest(message, "/v1/files", controller.signal)
143
+ : createWorkspaceRouteRequest(message, controller.signal);
144
+ const response = await (isFileRequest ? this.fileRoutes : this.workspaceRoutes).fetch(request);
145
+ await sendHttpTunnelResponse(message.id, response, (responseMessage) => this.send(responseMessage));
146
+ }
147
+ catch (err) {
148
+ this.send({
149
+ type: "response.error",
150
+ id: message.id,
151
+ code: "request_failed",
152
+ errorText: err instanceof Error ? err.message : "Workspace tunnel request failed",
153
+ });
154
+ }
155
+ finally {
156
+ this.running.delete(message.id);
157
+ }
158
+ }
159
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@messenger-agent/messenger-agent",
3
+ "version": "0.24.0-alpha.2",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "bin": {
8
+ "messenger-agent": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org"
16
+ },
17
+ "dependencies": {
18
+ "@hono/node-server": "^2.0.2",
19
+ "hono": "^4.12.18",
20
+ "ws": "^8.18.3",
21
+ "yaml": "^2.9.0",
22
+ "zod": "^4.4.3",
23
+ "@messenger-agent/shared": "0.24.0-alpha.2"
24
+ },
25
+ "devDependencies": {
26
+ "@types/ws": "^8.18.1"
27
+ },
28
+ "scripts": {
29
+ "build": "rm -rf dist && tsc -p tsconfig.json",
30
+ "build:sourcemap": "rm -rf dist && tsc -p tsconfig.json --sourceMap",
31
+ "start": "node dist/index.js",
32
+ "dev": "tsx src/index.ts"
33
+ }
34
+ }