@messenger-agent/client 0.24.0-alpha.2 → 0.24.0-alpha.4

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/service.js CHANGED
@@ -1,208 +1,3 @@
1
- import { access, chmod, mkdir, unlink, writeFile } from "node:fs/promises";
2
- import { userInfo } from "node:os";
3
- import { dirname, join } from "node:path";
4
- import { runCommand } from "./exec.js";
5
- export const serviceName = "coding-agent-client";
6
- export const launchdLabel = "vip.elevo.coding-agent.client";
7
- export function shellEscape(value) {
8
- return `'${value.replaceAll("'", "'\\''")}'`;
9
- }
10
- export function createSystemdService(options) {
11
- return [
12
- "[Unit]",
13
- "Description=Coding Agent Client",
14
- "After=network-online.target",
15
- "",
16
- "[Service]",
17
- "Type=simple",
18
- `ExecStart=${systemdEscape(options.serviceCommandPath)}`,
19
- "Restart=always",
20
- "RestartSec=5",
21
- `WorkingDirectory=${systemdEscape(options.workspacePath)}`,
22
- `Environment=${systemdEnv("AGENT_CONFIG_PATH", options.configPath)}`,
23
- "",
24
- "[Install]",
25
- "WantedBy=default.target",
26
- "",
27
- ].join("\n");
28
- }
29
- function systemdEscape(value) {
30
- return value.replaceAll("\\", "\\\\").replaceAll(" ", "\\x20");
31
- }
32
- function systemdEnv(name, value) {
33
- return `${name}=${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}`;
34
- }
35
- function xmlEscape(value) {
36
- return value
37
- .replaceAll("&", "&")
38
- .replaceAll("<", "&lt;")
39
- .replaceAll(">", "&gt;")
40
- .replaceAll('"', "&quot;")
41
- .replaceAll("'", "&apos;");
42
- }
43
- export function createLaunchdPlist(options) {
44
- const stdoutPath = join(options.dataDir, "logs", "client-service.out.log");
45
- const stderrPath = join(options.dataDir, "logs", "client-service.err.log");
46
- return [
47
- '<?xml version="1.0" encoding="UTF-8"?>',
48
- '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
49
- '<plist version="1.0">',
50
- "<dict>",
51
- " <key>Label</key>",
52
- ` <string>${xmlEscape(launchdLabel)}</string>`,
53
- " <key>ProgramArguments</key>",
54
- " <array>",
55
- ` <string>${xmlEscape(options.serviceCommandPath)}</string>`,
56
- " </array>",
57
- " <key>WorkingDirectory</key>",
58
- ` <string>${xmlEscape(options.workspacePath)}</string>`,
59
- " <key>EnvironmentVariables</key>",
60
- " <dict>",
61
- " <key>AGENT_CONFIG_PATH</key>",
62
- ` <string>${xmlEscape(options.configPath)}</string>`,
63
- " </dict>",
64
- " <key>RunAtLoad</key>",
65
- " <true/>",
66
- " <key>KeepAlive</key>",
67
- " <true/>",
68
- " <key>StandardOutPath</key>",
69
- ` <string>${xmlEscape(stdoutPath)}</string>`,
70
- " <key>StandardErrorPath</key>",
71
- ` <string>${xmlEscape(stderrPath)}</string>`,
72
- "</dict>",
73
- "</plist>",
74
- "",
75
- ].join("\n");
76
- }
77
- async function commandExists(command) {
78
- const pathDirs = (process.env.PATH ?? "").split(":").filter(Boolean);
79
- for (const pathDir of pathDirs) {
80
- try {
81
- await access(join(pathDir, command));
82
- return true;
83
- }
84
- catch {
85
- // continue
86
- }
87
- }
88
- return false;
89
- }
90
- const defaultLingerDependencies = { commandExists, runCommand };
91
- export async function ensureSystemdUserLinger(user, dependencies = defaultLingerDependencies) {
92
- if (!(await dependencies.commandExists("loginctl"))) {
93
- throw new Error(`loginctl is required to keep the user service running after logout. Enable linger for ${user} and retry installation.`);
94
- }
95
- if (await isLingerEnabled(user, dependencies.runCommand))
96
- return;
97
- console.log(`Enabling login persistence for ${user}`);
98
- const enable = await dependencies.runCommand("loginctl", ["enable-linger", user], {
99
- allowFailure: true,
100
- interactive: true,
101
- });
102
- if (enable.status !== 0) {
103
- throw new Error(`Unable to enable login persistence for ${user}. Run 'sudo loginctl enable-linger ${user}' and retry installation.`);
104
- }
105
- if (!(await isLingerEnabled(user, dependencies.runCommand))) {
106
- throw new Error(`Login persistence is still disabled for ${user}. Run 'sudo loginctl enable-linger ${user}' and retry installation.`);
107
- }
108
- }
109
- async function isLingerEnabled(user, command) {
110
- const result = await command("loginctl", ["show-user", user, "-p", "Linger"], { allowFailure: true });
111
- return result.status === 0 && result.stdout.trim() === "Linger=yes";
112
- }
113
- export async function installService(options) {
114
- const platform = options.platform ?? process.platform;
115
- if (platform === "linux")
116
- return installSystemdService(options);
117
- if (platform === "darwin")
118
- return installLaunchdService(options);
119
- throw new Error("Only Linux and macOS are supported");
120
- }
121
- async function installSystemdService(options) {
122
- if (!(await commandExists("systemctl"))) {
123
- throw new Error("systemctl is required to install the user service");
124
- }
125
- const user = options.user ?? process.env.USER ?? userInfo().username;
126
- await ensureSystemdUserLinger(user);
127
- const servicePath = join(process.env.HOME ?? "", ".config", "systemd", "user", `${serviceName}.service`);
128
- await mkdir(dirname(servicePath), { recursive: true, mode: 0o700 });
129
- await writeFile(servicePath, createSystemdService(options), { mode: 0o600 });
130
- await chmod(servicePath, 0o600);
131
- await runCommand("systemctl", ["--user", "daemon-reload"]);
132
- await runCommand("systemctl", ["--user", "enable", "--now", `${serviceName}.service`]);
133
- await runCommand("systemctl", ["--user", "restart", `${serviceName}.service`]);
134
- return {
135
- servicePath,
136
- commands: linuxCommands(),
137
- warnings: [],
138
- };
139
- }
140
- async function installLaunchdService(options) {
141
- if (!(await commandExists("launchctl"))) {
142
- throw new Error("launchctl is required to install the LaunchAgent");
143
- }
144
- const servicePath = join(process.env.HOME ?? "", "Library", "LaunchAgents", `${launchdLabel}.plist`);
145
- await mkdir(dirname(servicePath), { recursive: true, mode: 0o700 });
146
- await writeFile(servicePath, createLaunchdPlist(options), { mode: 0o600 });
147
- await chmod(servicePath, 0o600);
148
- const uid = options.uid ?? process.getuid?.();
149
- if (uid === undefined) {
150
- throw new Error("Unable to determine current uid for launchctl");
151
- }
152
- const domain = `gui/${uid}`;
153
- await runCommand("launchctl", ["bootout", domain, servicePath], { allowFailure: true });
154
- await runCommand("launchctl", ["bootstrap", domain, servicePath]);
155
- await runCommand("launchctl", ["kickstart", "-k", `${domain}/${launchdLabel}`]);
156
- return {
157
- servicePath,
158
- commands: darwinCommands(),
159
- warnings: [],
160
- };
161
- }
162
- export function linuxCommands() {
163
- return {
164
- status: `systemctl --user status ${serviceName}.service`,
165
- logs: `journalctl --user -u ${serviceName}.service -n 100 -f`,
166
- start: `systemctl --user start ${serviceName}.service`,
167
- restart: `systemctl --user restart ${serviceName}.service`,
168
- stop: `systemctl --user stop ${serviceName}.service`,
169
- uninstall: `systemctl --user disable --now ${serviceName}.service && rm -f ~/.config/systemd/user/${serviceName}.service && systemctl --user daemon-reload`,
170
- };
171
- }
172
- export function darwinCommands() {
173
- const domain = "gui/$(id -u)";
174
- const servicePath = `~/Library/LaunchAgents/${launchdLabel}.plist`;
175
- return {
176
- status: `launchctl print ${domain}/${launchdLabel}`,
177
- logs: "tail -f ~/.coding-agent/data/logs/client-service.out.log ~/.coding-agent/data/logs/client-service.err.log",
178
- start: `launchctl bootstrap ${domain} ${servicePath}`,
179
- restart: `launchctl bootout ${domain} ${servicePath} 2>/dev/null || true; launchctl bootstrap ${domain} ${servicePath}`,
180
- stop: `launchctl bootout ${domain} ${servicePath}`,
181
- uninstall: `launchctl bootout ${domain} ${servicePath}; rm -f ${servicePath}`,
182
- };
183
- }
184
- export async function uninstallService(platform = process.platform) {
185
- if (platform === "linux") {
186
- await runCommand("systemctl", ["--user", "disable", "--now", `${serviceName}.service`], { allowFailure: true });
187
- await unlink(join(process.env.HOME ?? "", ".config", "systemd", "user", `${serviceName}.service`)).catch((err) => {
188
- if (err.code !== "ENOENT")
189
- throw err;
190
- });
191
- await runCommand("systemctl", ["--user", "daemon-reload"], { allowFailure: true });
192
- return;
193
- }
194
- if (platform === "darwin") {
195
- const uid = process.getuid?.();
196
- if (uid === undefined) {
197
- throw new Error("Unable to determine current uid for launchctl");
198
- }
199
- const servicePath = join(process.env.HOME ?? "", "Library", "LaunchAgents", `${launchdLabel}.plist`);
200
- await runCommand("launchctl", ["bootout", `gui/${uid}`, servicePath], { allowFailure: true });
201
- await unlink(servicePath).catch((err) => {
202
- if (err.code !== "ENOENT")
203
- throw err;
204
- });
205
- return;
206
- }
207
- throw new Error("Only Linux and macOS are supported");
208
- }
1
+ import{access as f,chmod as u,mkdir as d,unlink as m,writeFile as g}from"node:fs/promises";import{userInfo as h}from"node:os";import{dirname as w,join as i}from"node:path";import{runCommand as a}from"./exec.js";const n="coding-agent-client",o="vip.elevo.coding-agent.client";function T(e){return`'${e.replaceAll("'","'\\''")}'`}function $(e){return["[Unit]","Description=Coding Agent Client","After=network-online.target","","[Service]","Type=simple",`ExecStart=${p(e.serviceCommandPath)}`,"Restart=always","RestartSec=5",`WorkingDirectory=${p(e.workspacePath)}`,`Environment=${v("AGENT_CONFIG_PATH",e.configPath)}`,"","[Install]","WantedBy=default.target",""].join(`
2
+ `)}function p(e){return e.replaceAll("\\","\\\\").replaceAll(" ","\\x20")}function v(e,t){return`${e}=${t.replaceAll("\\","\\\\").replaceAll('"','\\"')}`}function l(e){return e.replaceAll("&","&amp;").replaceAll("<","&lt;").replaceAll(">","&gt;").replaceAll('"',"&quot;").replaceAll("'","&apos;")}function E(e){const t=i(e.dataDir,"logs","client-service.out.log"),r=i(e.dataDir,"logs","client-service.err.log");return['<?xml version="1.0" encoding="UTF-8"?>','<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">','<plist version="1.0">',"<dict>"," <key>Label</key>",` <string>${l(o)}</string>`," <key>ProgramArguments</key>"," <array>",` <string>${l(e.serviceCommandPath)}</string>`," </array>"," <key>WorkingDirectory</key>",` <string>${l(e.workspacePath)}</string>`," <key>EnvironmentVariables</key>"," <dict>"," <key>AGENT_CONFIG_PATH</key>",` <string>${l(e.configPath)}</string>`," </dict>"," <key>RunAtLoad</key>"," <true/>"," <key>KeepAlive</key>"," <true/>"," <key>StandardOutPath</key>",` <string>${l(t)}</string>`," <key>StandardErrorPath</key>",` <string>${l(r)}</string>`,"</dict>","</plist>",""].join(`
3
+ `)}async function c(e){const t=(process.env.PATH??"").split(":").filter(Boolean);for(const r of t)try{return await f(i(r,e)),!0}catch{}return!1}const b={commandExists:c,runCommand:a};async function k(e,t=b){if(!await t.commandExists("loginctl"))throw new Error(`loginctl is required to keep the user service running after logout. Enable linger for ${e} and retry installation.`);if(await y(e,t.runCommand))return;if(console.log(`Enabling login persistence for ${e}`),(await t.runCommand("loginctl",["enable-linger",e],{allowFailure:!0,interactive:!0})).status!==0)throw new Error(`Unable to enable login persistence for ${e}. Run 'sudo loginctl enable-linger ${e}' and retry installation.`);if(!await y(e,t.runCommand))throw new Error(`Login persistence is still disabled for ${e}. Run 'sudo loginctl enable-linger ${e}' and retry installation.`)}async function y(e,t){const r=await t("loginctl",["show-user",e,"-p","Linger"],{allowFailure:!0});return r.status===0&&r.stdout.trim()==="Linger=yes"}async function F(e){const t=e.platform??process.platform;if(t==="linux")return A(e);if(t==="darwin")return L(e);throw new Error("Only Linux and macOS are supported")}async function A(e){if(!await c("systemctl"))throw new Error("systemctl is required to install the user service");const t=e.user??process.env.USER??h().username;await k(t);const r=i(process.env.HOME??"",".config","systemd","user",`${n}.service`);return await d(w(r),{recursive:!0,mode:448}),await g(r,$(e),{mode:384}),await u(r,384),await a("systemctl",["--user","daemon-reload"]),await a("systemctl",["--user","enable","--now",`${n}.service`]),await a("systemctl",["--user","restart",`${n}.service`]),{servicePath:r,commands:P(),warnings:[]}}async function L(e){if(!await c("launchctl"))throw new Error("launchctl is required to install the LaunchAgent");const t=i(process.env.HOME??"","Library","LaunchAgents",`${o}.plist`);await d(w(t),{recursive:!0,mode:448}),await g(t,E(e),{mode:384}),await u(t,384);const r=e.uid??process.getuid?.();if(r===void 0)throw new Error("Unable to determine current uid for launchctl");const s=`gui/${r}`;return await a("launchctl",["bootout",s,t],{allowFailure:!0}),await a("launchctl",["bootstrap",s,t]),await a("launchctl",["kickstart","-k",`${s}/${o}`]),{servicePath:t,commands:x(),warnings:[]}}function P(){return{status:`systemctl --user status ${n}.service`,logs:`journalctl --user -u ${n}.service -n 100 -f`,start:`systemctl --user start ${n}.service`,restart:`systemctl --user restart ${n}.service`,stop:`systemctl --user stop ${n}.service`,uninstall:`systemctl --user disable --now ${n}.service && rm -f ~/.config/systemd/user/${n}.service && systemctl --user daemon-reload`}}function x(){const e="gui/$(id -u)",t=`~/Library/LaunchAgents/${o}.plist`;return{status:`launchctl print ${e}/${o}`,logs:"tail -f ~/.coding-agent/data/logs/client-service.out.log ~/.coding-agent/data/logs/client-service.err.log",start:`launchctl bootstrap ${e} ${t}`,restart:`launchctl bootout ${e} ${t} 2>/dev/null || true; launchctl bootstrap ${e} ${t}`,stop:`launchctl bootout ${e} ${t}`,uninstall:`launchctl bootout ${e} ${t}; rm -f ${t}`}}async function N(e=process.platform){if(e==="linux"){await a("systemctl",["--user","disable","--now",`${n}.service`],{allowFailure:!0}),await m(i(process.env.HOME??"",".config","systemd","user",`${n}.service`)).catch(t=>{if(t.code!=="ENOENT")throw t}),await a("systemctl",["--user","daemon-reload"],{allowFailure:!0});return}if(e==="darwin"){const t=process.getuid?.();if(t===void 0)throw new Error("Unable to determine current uid for launchctl");const r=i(process.env.HOME??"","Library","LaunchAgents",`${o}.plist`);await a("launchctl",["bootout",`gui/${t}`,r],{allowFailure:!0}),await m(r).catch(s=>{if(s.code!=="ENOENT")throw s});return}throw new Error("Only Linux and macOS are supported")}export{E as createLaunchdPlist,$ as createSystemdService,x as darwinCommands,k as ensureSystemdUserLinger,F as installService,o as launchdLabel,P as linuxCommands,n as serviceName,T as shellEscape,N as uninstallService};
@@ -1,282 +1 @@
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
- }
1
+ import{spawn as P}from"node:child_process";import{randomUUID as T}from"node:crypto";import{createRequire as x}from"node:module";import{dirname as d,join as p}from"node:path";import{parse as S}from"yaml";import{readFile as M,unlink as y}from"node:fs/promises";import{readControlSocketPath as E,startControlServer as I}from"./control.js";import{MaintenanceScheduler as C}from"./maintenance.js";import{readUpgradeChannel as $}from"./config-file.js";import{currentBundledSkillsDir as b,defaultAgentHomes as k,installRuntime as N,syncBundledSkills as O}from"./runtime.js";import{darwinCommands as F,launchdLabel as v,linuxCommands as G,serviceName as f}from"./service.js";import{runCommand as l}from"./exec.js";import{AutoUpgradeScheduler as L}from"./auto-upgrade.js";import{isAgentActivityResponse as R}from"@messenger-agent/shared/agent-activity";const w=x(import.meta.url);function _(){return[{name:"codex",entry:w.resolve("@messenger-agent/codex-agent")},{name:"claude",entry:w.resolve("@messenger-agent/claude-agent")},{name:"workspace",entry:w.resolve("@messenger-agent/messenger-agent")}]}function B(s,t){let e=s;for(const r of t){if(!e||typeof e!="object"||Array.isArray(e))return;e=e[r]}return e}async function j(s){const t=S(await M(s,"utf8")),e=B(t,["workspaces"]);if(Array.isArray(e)){const r=e.find(i=>!!i&&typeof i=="object"&&!Array.isArray(i)&&typeof i.path=="string");if(typeof r?.path=="string"&&r.path.trim())return r.path}return d(s)}class q{options;stopping=!1;running;restartBaseMs;restartMaxMs;constructor(t){this.options=t,this.running=(t.agents??_()).map(e=>({definition:e,restartAttempts:0})),this.restartBaseMs=t.restartBaseMs??1e3,this.restartMaxMs=t.restartMaxMs??3e4}start(){for(const t of this.running)this.startAgent(t)}async stop(){this.stopping=!0;for(const t of this.running)t.restartTimer&&clearTimeout(t.restartTimer),t.process?.kill("SIGTERM");await Promise.all(this.running.map(t=>t.process).filter(t=>!!t&&t.exitCode===null&&!t.killed).map(t=>new Promise(e=>{const r=setTimeout(()=>{t.kill("SIGKILL"),e()},8e3);t.once("exit",()=>{clearTimeout(r),e()})})))}restartAgent(t){const e=this.running.find(({definition:r})=>r.name===t);return e?(e.restartPromise||(e.restartPromise=this.restartAgentProcess(e).finally(()=>{e.restartPromise=void 0})),e.restartPromise):Promise.reject(new Error(`Agent is not managed by this client: ${t}`))}async activityStatus(t=1e3){const[e,r]=await Promise.all([this.agentActivityStatus("codex",t),this.agentActivityStatus("claude",t)]),i=[e,r].filter(n=>n.available);return{active:i.reduce((n,a)=>n+a.active,0),waiting:i.reduce((n,a)=>n+a.waiting,0),agents:{codex:e,claude:r}}}agentActivityStatus(t,e){const r=this.running.find(({definition:i})=>i.name===t)?.process;return!r||!r.connected||!r.send?Promise.resolve({available:!1,error:`${t} agent is not connected`}):new Promise(i=>{const n=T(),a={type:"client.activity.request",requestId:n};let u=!1;const c=o=>{u||(u=!0,clearTimeout(A),r.off("message",m),i(o))},m=o=>{!R(o)||o.requestId!==n||o.agent!==t||c({available:!0,...o.snapshot})},A=setTimeout(()=>c({available:!1,error:`${t} agent did not respond`}),e);r.on("message",m),r.send(a,o=>{o&&c({available:!1,error:o.message})})})}async restartAgentProcess(t){t.restartTimer&&(clearTimeout(t.restartTimer),t.restartTimer=void 0);const e=t.process;if(e&&e.exitCode===null&&!e.killed&&(e.kill("SIGTERM"),await H(e)),this.stopping)throw new Error("Client service is stopping");t.restartAttempts=0,this.startAgent(t)}startAgent(t){const e={...process.env,AGENT_CONFIG_PATH:this.options.configPath};t.definition.name==="workspace"&&(delete e.OPENAI_API_KEY,delete e.CODEX_API_KEY,delete e.ANTHROPIC_API_KEY);const r=(this.options.spawnProcess??P)(process.execPath,[t.definition.entry],{cwd:this.options.workspacePath,env:e,stdio:["inherit","inherit","inherit","ipc"]});t.process=r,console.log(`[client] started ${t.definition.name}-agent pid=${r.pid}`),r.once("exit",(i,n)=>{if(t.process=void 0,this.stopping||t.restartPromise)return;const a=Math.min(this.restartBaseMs*2**t.restartAttempts,this.restartMaxMs);t.restartAttempts+=1,console.error(`[client] ${t.definition.name}-agent exited with code=${i??"null"} signal=${n??"null"}; restarting in ${a}ms`),t.restartTimer=setTimeout(()=>{t.restartTimer=void 0,this.startAgent(t)},a)})}}async function nt(s){await O(b(),k());const t=await j(s),e=new q({configPath:s,workspacePath:t}),r=await E(s),i=new C({statePath:p(d(r),"maintenance.json"),getActivity:()=>e.activityStatus(),execute:(c,m)=>U(c,s,e,m)});await i.start();const n=new L({statePath:p(d(r),"auto-upgrade.json"),maintenance:i,getChannel:()=>$(s)});await n.start();const a=await I(r,{restartAgent:c=>e.restartAgent(c),getActivity:()=>e.activityStatus(),maintenance:i});e.start();const u=async()=>{n.stop(),i.stop(),await a.close(),await e.stop(),process.exit(0)};process.once("SIGINT",()=>{u()}),process.once("SIGTERM",()=>{u()})}async function U(s,t,e,r){if(s.type==="upgrade"){await N({version:s.version,configPath:t}),await r("restarting"),await h(g().restart);return}if(s.type==="restart"&&s.agent){await e.restartAgent(s.agent);return}if(s.type==="restart"){await r("restarting"),await h(g().restart);return}if(s.type==="stop"){await r("stopping"),await h(g().stop);return}await r("stopping"),await K()}function g(){return process.platform==="darwin"?F():G()}async function h(s){const t=await l("/bin/sh",["-lc",s],{allowFailure:!0});if(t.status!==0)throw new Error(t.stderr||t.stdout||`Service command failed: ${s}`)}async function K(){const s=process.env.HOME??"";if(process.platform==="linux"){await l("systemctl",["--user","disable",`${f}.service`],{allowFailure:!0}),await y(p(s,".config","systemd","user",`${f}.service`)).catch(t=>{if(t.code!=="ENOENT")throw t}),await l("systemctl",["--user","daemon-reload"],{allowFailure:!0}),await l("systemctl",["--user","stop",`${f}.service`],{allowFailure:!0});return}if(process.platform==="darwin"){const t=process.getuid?.();if(t===void 0)throw new Error("Unable to determine current uid for launchctl");const e=p(s,"Library","LaunchAgents",`${v}.plist`);await y(e).catch(r=>{if(r.code!=="ENOENT")throw r}),await l("launchctl",["bootout",`gui/${t}/${v}`],{allowFailure:!0});return}throw new Error("Only Linux and macOS are supported")}function H(s){return new Promise(t=>{const e=setTimeout(()=>{s.kill("SIGKILL")},8e3);s.once("exit",()=>{clearTimeout(e),t()})})}export{q as AgentSupervisor,j as readWorkspacePathFromConfig,_ as resolveAgentEntries,nt as runSupervisor};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@messenger-agent/client",
3
- "version": "0.24.0-alpha.2",
3
+ "version": "0.24.0-alpha.4",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -17,10 +17,10 @@
17
17
  "dependencies": {
18
18
  "cac": "^7.0.0",
19
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"
20
+ "@messenger-agent/claude-agent": "0.24.0-alpha.4",
21
+ "@messenger-agent/messenger-agent": "0.24.0-alpha.4",
22
+ "@messenger-agent/codex-agent": "0.24.0-alpha.4",
23
+ "@messenger-agent/shared": "0.24.0-alpha.4"
24
24
  },
25
25
  "scripts": {
26
26
  "build": "rm -rf dist && tsc -p tsconfig.json && node ../../scripts/copy-client-assets.mjs && chmod +x dist/index.js",