@messenger-agent/client 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.
@@ -0,0 +1,282 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { createRequire } from "node:module";
4
+ import { dirname, join } from "node:path";
5
+ import { parse } from "yaml";
6
+ import { readFile, unlink } from "node:fs/promises";
7
+ import { readControlSocketPath, startControlServer } from "./control.js";
8
+ import { MaintenanceScheduler } from "./maintenance.js";
9
+ import { readUpgradeChannel } from "./config-file.js";
10
+ import { currentBundledSkillsDir, defaultAgentHomes, installRuntime, syncBundledSkills } from "./runtime.js";
11
+ import { darwinCommands, launchdLabel, linuxCommands, serviceName } from "./service.js";
12
+ import { runCommand } from "./exec.js";
13
+ import { AutoUpgradeScheduler } from "./auto-upgrade.js";
14
+ import { isAgentActivityResponse, } from "@messenger-agent/shared/agent-activity";
15
+ const require = createRequire(import.meta.url);
16
+ export function resolveAgentEntries() {
17
+ return [
18
+ { name: "codex", entry: require.resolve("@messenger-agent/codex-agent") },
19
+ { name: "claude", entry: require.resolve("@messenger-agent/claude-agent") },
20
+ { name: "workspace", entry: require.resolve("@messenger-agent/messenger-agent") },
21
+ ];
22
+ }
23
+ function getByPath(root, path) {
24
+ let value = root;
25
+ for (const key of path) {
26
+ if (!value || typeof value !== "object" || Array.isArray(value))
27
+ return undefined;
28
+ value = value[key];
29
+ }
30
+ return value;
31
+ }
32
+ export async function readWorkspacePathFromConfig(configPath) {
33
+ const config = parse(await readFile(configPath, "utf8"));
34
+ const workspaces = getByPath(config, ["workspaces"]);
35
+ if (Array.isArray(workspaces)) {
36
+ const firstWorkspace = workspaces.find((workspace) => !!workspace && typeof workspace === "object" && !Array.isArray(workspace) && typeof workspace.path === "string");
37
+ if (typeof firstWorkspace?.path === "string" && firstWorkspace.path.trim())
38
+ return firstWorkspace.path;
39
+ }
40
+ return dirname(configPath);
41
+ }
42
+ export class AgentSupervisor {
43
+ options;
44
+ stopping = false;
45
+ running;
46
+ restartBaseMs;
47
+ restartMaxMs;
48
+ constructor(options) {
49
+ this.options = options;
50
+ this.running = (options.agents ?? resolveAgentEntries()).map((definition) => ({
51
+ definition,
52
+ restartAttempts: 0,
53
+ }));
54
+ this.restartBaseMs = options.restartBaseMs ?? 1000;
55
+ this.restartMaxMs = options.restartMaxMs ?? 30000;
56
+ }
57
+ start() {
58
+ for (const agent of this.running)
59
+ this.startAgent(agent);
60
+ }
61
+ async stop() {
62
+ this.stopping = true;
63
+ for (const agent of this.running) {
64
+ if (agent.restartTimer)
65
+ clearTimeout(agent.restartTimer);
66
+ agent.process?.kill("SIGTERM");
67
+ }
68
+ await Promise.all(this.running
69
+ .map((agent) => agent.process)
70
+ .filter((child) => !!child && child.exitCode === null && !child.killed)
71
+ .map((child) => new Promise((resolve) => {
72
+ const timer = setTimeout(() => {
73
+ child.kill("SIGKILL");
74
+ resolve();
75
+ }, 8000);
76
+ child.once("exit", () => {
77
+ clearTimeout(timer);
78
+ resolve();
79
+ });
80
+ })));
81
+ }
82
+ restartAgent(name) {
83
+ const agent = this.running.find(({ definition }) => definition.name === name);
84
+ if (!agent)
85
+ return Promise.reject(new Error(`Agent is not managed by this client: ${name}`));
86
+ if (agent.restartPromise)
87
+ return agent.restartPromise;
88
+ agent.restartPromise = this.restartAgentProcess(agent).finally(() => {
89
+ agent.restartPromise = undefined;
90
+ });
91
+ return agent.restartPromise;
92
+ }
93
+ async activityStatus(timeoutMs = 1000) {
94
+ const [codex, claude] = await Promise.all([
95
+ this.agentActivityStatus("codex", timeoutMs),
96
+ this.agentActivityStatus("claude", timeoutMs),
97
+ ]);
98
+ const available = [codex, claude].filter((status) => status.available);
99
+ return {
100
+ active: available.reduce((sum, status) => sum + status.active, 0),
101
+ waiting: available.reduce((sum, status) => sum + status.waiting, 0),
102
+ agents: { codex, claude },
103
+ };
104
+ }
105
+ agentActivityStatus(name, timeoutMs) {
106
+ const child = this.running.find(({ definition }) => definition.name === name)?.process;
107
+ if (!child || !child.connected || !child.send) {
108
+ return Promise.resolve({ available: false, error: `${name} agent is not connected` });
109
+ }
110
+ return new Promise((resolve) => {
111
+ const requestId = randomUUID();
112
+ const request = { type: "client.activity.request", requestId };
113
+ let settled = false;
114
+ const finish = (status) => {
115
+ if (settled)
116
+ return;
117
+ settled = true;
118
+ clearTimeout(timer);
119
+ child.off("message", onMessage);
120
+ resolve(status);
121
+ };
122
+ const onMessage = (message) => {
123
+ if (!isAgentActivityResponse(message) || message.requestId !== requestId || message.agent !== name)
124
+ return;
125
+ finish({ available: true, ...message.snapshot });
126
+ };
127
+ const timer = setTimeout(() => finish({ available: false, error: `${name} agent did not respond` }), timeoutMs);
128
+ child.on("message", onMessage);
129
+ child.send(request, (err) => {
130
+ if (err)
131
+ finish({ available: false, error: err.message });
132
+ });
133
+ });
134
+ }
135
+ async restartAgentProcess(agent) {
136
+ if (agent.restartTimer) {
137
+ clearTimeout(agent.restartTimer);
138
+ agent.restartTimer = undefined;
139
+ }
140
+ const child = agent.process;
141
+ if (child && child.exitCode === null && !child.killed) {
142
+ child.kill("SIGTERM");
143
+ await waitForExit(child);
144
+ }
145
+ if (this.stopping)
146
+ throw new Error("Client service is stopping");
147
+ agent.restartAttempts = 0;
148
+ this.startAgent(agent);
149
+ }
150
+ startAgent(agent) {
151
+ const env = {
152
+ ...process.env,
153
+ AGENT_CONFIG_PATH: this.options.configPath,
154
+ };
155
+ if (agent.definition.name === "workspace") {
156
+ delete env.OPENAI_API_KEY;
157
+ delete env.CODEX_API_KEY;
158
+ delete env.ANTHROPIC_API_KEY;
159
+ }
160
+ const child = (this.options.spawnProcess ?? spawn)(process.execPath, [agent.definition.entry], {
161
+ cwd: this.options.workspacePath,
162
+ env,
163
+ stdio: ["inherit", "inherit", "inherit", "ipc"],
164
+ });
165
+ agent.process = child;
166
+ console.log(`[client] started ${agent.definition.name}-agent pid=${child.pid}`);
167
+ child.once("exit", (code, signal) => {
168
+ agent.process = undefined;
169
+ if (this.stopping || agent.restartPromise)
170
+ return;
171
+ const delay = Math.min(this.restartBaseMs * 2 ** agent.restartAttempts, this.restartMaxMs);
172
+ agent.restartAttempts += 1;
173
+ console.error(`[client] ${agent.definition.name}-agent exited with code=${code ?? "null"} signal=${signal ?? "null"}; restarting in ${delay}ms`);
174
+ agent.restartTimer = setTimeout(() => {
175
+ agent.restartTimer = undefined;
176
+ this.startAgent(agent);
177
+ }, delay);
178
+ });
179
+ }
180
+ }
181
+ export async function runSupervisor(configPath) {
182
+ await syncBundledSkills(currentBundledSkillsDir(), defaultAgentHomes());
183
+ const workspacePath = await readWorkspacePathFromConfig(configPath);
184
+ const supervisor = new AgentSupervisor({ configPath, workspacePath });
185
+ const socketPath = await readControlSocketPath(configPath);
186
+ const maintenance = new MaintenanceScheduler({
187
+ statePath: join(dirname(socketPath), "maintenance.json"),
188
+ getActivity: () => supervisor.activityStatus(),
189
+ execute: (operation, markServiceExit) => executeMaintenanceOperation(operation, configPath, supervisor, markServiceExit),
190
+ });
191
+ await maintenance.start();
192
+ const autoUpgrade = new AutoUpgradeScheduler({
193
+ statePath: join(dirname(socketPath), "auto-upgrade.json"),
194
+ maintenance,
195
+ getChannel: () => readUpgradeChannel(configPath),
196
+ });
197
+ await autoUpgrade.start();
198
+ const controlServer = await startControlServer(socketPath, {
199
+ restartAgent: (agent) => supervisor.restartAgent(agent),
200
+ getActivity: () => supervisor.activityStatus(),
201
+ maintenance,
202
+ });
203
+ supervisor.start();
204
+ const stop = async () => {
205
+ autoUpgrade.stop();
206
+ maintenance.stop();
207
+ await controlServer.close();
208
+ await supervisor.stop();
209
+ process.exit(0);
210
+ };
211
+ process.once("SIGINT", () => void stop());
212
+ process.once("SIGTERM", () => void stop());
213
+ }
214
+ async function executeMaintenanceOperation(operation, configPath, supervisor, markServiceExit) {
215
+ if (operation.type === "upgrade") {
216
+ await installRuntime({ version: operation.version, configPath });
217
+ await markServiceExit("restarting");
218
+ await runServiceCommand(serviceCommands().restart);
219
+ return;
220
+ }
221
+ if (operation.type === "restart" && operation.agent) {
222
+ await supervisor.restartAgent(operation.agent);
223
+ return;
224
+ }
225
+ if (operation.type === "restart") {
226
+ await markServiceExit("restarting");
227
+ await runServiceCommand(serviceCommands().restart);
228
+ return;
229
+ }
230
+ if (operation.type === "stop") {
231
+ await markServiceExit("stopping");
232
+ await runServiceCommand(serviceCommands().stop);
233
+ return;
234
+ }
235
+ await markServiceExit("stopping");
236
+ await uninstallRunningService();
237
+ }
238
+ function serviceCommands() {
239
+ return process.platform === "darwin" ? darwinCommands() : linuxCommands();
240
+ }
241
+ async function runServiceCommand(command) {
242
+ const result = await runCommand("/bin/sh", ["-lc", command], { allowFailure: true });
243
+ if (result.status !== 0)
244
+ throw new Error(result.stderr || result.stdout || `Service command failed: ${command}`);
245
+ }
246
+ async function uninstallRunningService() {
247
+ const home = process.env.HOME ?? "";
248
+ if (process.platform === "linux") {
249
+ await runCommand("systemctl", ["--user", "disable", `${serviceName}.service`], { allowFailure: true });
250
+ await unlink(join(home, ".config", "systemd", "user", `${serviceName}.service`)).catch((err) => {
251
+ if (err.code !== "ENOENT")
252
+ throw err;
253
+ });
254
+ await runCommand("systemctl", ["--user", "daemon-reload"], { allowFailure: true });
255
+ await runCommand("systemctl", ["--user", "stop", `${serviceName}.service`], { allowFailure: true });
256
+ return;
257
+ }
258
+ if (process.platform === "darwin") {
259
+ const uid = process.getuid?.();
260
+ if (uid === undefined)
261
+ throw new Error("Unable to determine current uid for launchctl");
262
+ const servicePath = join(home, "Library", "LaunchAgents", `${launchdLabel}.plist`);
263
+ await unlink(servicePath).catch((err) => {
264
+ if (err.code !== "ENOENT")
265
+ throw err;
266
+ });
267
+ await runCommand("launchctl", ["bootout", `gui/${uid}/${launchdLabel}`], { allowFailure: true });
268
+ return;
269
+ }
270
+ throw new Error("Only Linux and macOS are supported");
271
+ }
272
+ function waitForExit(child) {
273
+ return new Promise((resolve) => {
274
+ const timer = setTimeout(() => {
275
+ child.kill("SIGKILL");
276
+ }, 8000);
277
+ child.once("exit", () => {
278
+ clearTimeout(timer);
279
+ resolve();
280
+ });
281
+ });
282
+ }
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@messenger-agent/client",
3
+ "version": "0.24.0-alpha.2",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "bin": {
8
+ "coding-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
+ "cac": "^7.0.0",
19
+ "yaml": "^2.9.0",
20
+ "@messenger-agent/claude-agent": "0.24.0-alpha.2",
21
+ "@messenger-agent/messenger-agent": "0.24.0-alpha.2",
22
+ "@messenger-agent/codex-agent": "0.24.0-alpha.2",
23
+ "@messenger-agent/shared": "0.24.0-alpha.2"
24
+ },
25
+ "scripts": {
26
+ "build": "rm -rf dist && tsc -p tsconfig.json && node ../../scripts/copy-client-assets.mjs && chmod +x dist/index.js",
27
+ "build:sourcemap": "rm -rf dist && tsc -p tsconfig.json --sourceMap && node ../../scripts/copy-client-assets.mjs && chmod +x dist/index.js",
28
+ "start": "node dist/index.js",
29
+ "dev": "tsx src/index.ts"
30
+ }
31
+ }