@anusornneal/chat-relay 0.7.2 → 0.8.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/cli/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { clearConfig, configPath, loadConfig, saveConfig } from "./config.mjs";
2
2
  import { login, requestJson } from "./device-login.mjs";
3
- import { remote, validateConfig } from "./remote.mjs";
3
+ import { protocolCompatibility, remote, validateConfig } from "./remote.mjs";
4
4
 
5
5
  function parseOptions(args) {
6
6
  const options = {};
@@ -31,6 +31,9 @@ Usage:
31
31
  chat-relay remote [options] Connect this computer to ChatGPT
32
32
  chat-relay login [options] Sign in and register this computer
33
33
  chat-relay status Show account and connection status
34
+ chat-relay drain Stop accepting new long-running work
35
+ chat-relay resume Resume normal work admission
36
+ chat-relay restart Restart after drain completes
34
37
  chat-relay logout Revoke this computer login
35
38
  chat-relay help Show this help
36
39
 
@@ -124,6 +127,15 @@ async function status() {
124
127
  "Scopes: " +
125
128
  (Array.isArray(agentStatus.data.scopes) ? agentStatus.data.scopes.join(",") : "-"),
126
129
  );
130
+ const connection = agentStatus.data.connection || {};
131
+ const compatibility = protocolCompatibility(connection.protocolVersion, agentStatus.data.expectedProtocolVersion);
132
+ console.log("Version: " + (connection.agentVersion || "-"));
133
+ console.log(`Protocol: ${connection.protocolVersion ?? "?"}/${agentStatus.data.expectedProtocolVersion ?? "?"} (${compatibility})`);
134
+ if (agentStatus.data.lifecycle) {
135
+ const lifecycle = agentStatus.data.lifecycle;
136
+ console.log("Lifecycle: " + lifecycle.state + (lifecycle.readyToRestart ? " (ready to restart)" : ""));
137
+ if (lifecycle.work?.total) console.log("ActiveWork: " + lifecycle.work.total);
138
+ }
127
139
  }
128
140
  console.log("Files: " + config.allowedRoots);
129
141
  console.log("Desktop: " + (config.desktopEnabled === true ? "enabled" : "disabled"));
@@ -139,6 +151,31 @@ async function status() {
139
151
  }
140
152
  return 0;
141
153
  }
154
+ async function lifecycleAction(action) {
155
+ const config = loadConfig();
156
+ if (!config) {
157
+ console.error("Chat Relay: not signed in");
158
+ return 1;
159
+ }
160
+ const result = await requestJson(config.relayUrl + "/relay?agentId=" + encodeURIComponent(config.agentId), {
161
+ method: "POST",
162
+ headers: { authorization: "Bearer " + config.userToken, "content-type": "application/json" },
163
+ body: JSON.stringify({ payload: { action: "agent.lifecycle." + action } }),
164
+ }).catch((error) => ({ response: { ok: false, status: 0 }, data: { error: error.message } }));
165
+ if (!result.response.ok) {
166
+ console.error("Lifecycle action failed: " + (result.data?.error || ("HTTP " + result.response.status)));
167
+ return 1;
168
+ }
169
+ const payload = result.data?.payload || result.data;
170
+ if (payload?.ok === false) {
171
+ console.error("Lifecycle action rejected: " + (payload.error || "unknown_error"));
172
+ if (payload.lifecycle?.work?.total) console.error("Active work remaining: " + payload.lifecycle.work.total);
173
+ return 1;
174
+ }
175
+ console.log("Agent lifecycle: " + (payload?.lifecycle?.state || "unknown"));
176
+ return 0;
177
+ }
178
+
142
179
  async function logout() {
143
180
  const config = loadConfig();
144
181
  if (!config) {
@@ -190,6 +227,9 @@ export async function runCli(argv = process.argv.slice(2)) {
190
227
 
191
228
  if (command === "logout") return logout();
192
229
  if (command === "status") return status();
230
+ if (command === "drain") return lifecycleAction("drain");
231
+ if (command === "resume") return lifecycleAction("resume");
232
+ if (command === "restart") return lifecycleAction("restart");
193
233
  if (command === "remote") {
194
234
  await remote(options);
195
235
  return 0;
package/cli/remote.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { spawn } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
2
  import { fileURLToPath } from "node:url";
3
3
  import path from "node:path";
4
4
  import { loadConfig, saveConfig } from "./config.mjs";
@@ -7,6 +7,32 @@ import { login, requestJson } from "./device-login.mjs";
7
7
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
8
  let stopping = false;
9
9
 
10
+ export function shouldRestartAgent(result, stoppingNow = false) {
11
+ return !stoppingNow && result?.code !== 2 && result?.code !== 3;
12
+ }
13
+
14
+ export function protocolCompatibility(agentProtocolVersion, expectedProtocolVersion) {
15
+ if (agentProtocolVersion === null || agentProtocolVersion === undefined || expectedProtocolVersion === null || expectedProtocolVersion === undefined) return "unknown";
16
+ if (!Number.isInteger(Number(agentProtocolVersion)) || !Number.isInteger(Number(expectedProtocolVersion))) return "unknown";
17
+ return Number(agentProtocolVersion) === Number(expectedProtocolVersion) ? "compatible" : "incompatible";
18
+ }
19
+
20
+ function terminateChildTree(child) {
21
+ if (child.killed) return;
22
+ if (process.platform === "win32" && child.pid) {
23
+ const pid = child.pid;
24
+ execFile("taskkill.exe", ["/PID", String(pid), "/T"], { windowsHide: true }, () => {
25
+ setTimeout(() => {
26
+ if (child.exitCode === null && child.signalCode === null) {
27
+ execFile("taskkill.exe", ["/PID", String(pid), "/T", "/F"], { windowsHide: true }, () => {});
28
+ }
29
+ }, 1000);
30
+ });
31
+ return;
32
+ }
33
+ child.kill("SIGTERM");
34
+ }
35
+
10
36
  async function validateConfig(config) {
11
37
  if (!config?.relayUrl || !config?.userToken || !config?.agentToken || !config?.agentId) return false;
12
38
  const { response } = await requestJson(`${config.relayUrl}/auth/me`, {
@@ -49,6 +75,7 @@ function spawnAgent(config) {
49
75
  }
50
76
 
51
77
  export async function remote(options = {}) {
78
+ stopping = false;
52
79
  let config = loadConfig();
53
80
  if (!(await validateConfig(config))) {
54
81
  console.log("No valid Chat Relay login found. Starting sign in...");
@@ -71,7 +98,7 @@ export async function remote(options = {}) {
71
98
 
72
99
  const stop = () => {
73
100
  stopping = true;
74
- if (!child.killed) child.kill();
101
+ terminateChildTree(child);
75
102
  };
76
103
  process.once("SIGINT", stop);
77
104
  process.once("SIGTERM", stop);
@@ -84,10 +111,28 @@ export async function remote(options = {}) {
84
111
  process.removeListener("SIGTERM", stop);
85
112
  if (stopping) break;
86
113
 
87
- console.error(
88
- `Agent stopped (code=${result.code ?? "null"}, signal=${result.signal ?? "none"}). Restarting in 2s...`,
89
- );
90
- await new Promise((resolve) => setTimeout(resolve, 2000));
114
+ if (result.code === 2) {
115
+ stopping = true;
116
+ console.error("Agent authorization is no longer valid. Run chat-relay login --force, then chat-relay remote.");
117
+ break;
118
+ }
119
+ if (result.code === 3) {
120
+ stopping = true;
121
+ console.error("Agent protocol is incompatible with the relay. Update Chat Relay, then run chat-relay remote again.");
122
+ break;
123
+ }
124
+
125
+ if (result.code === 4) {
126
+ console.log("Agent restart requested. Restarting local agent...");
127
+ continue;
128
+ }
129
+
130
+ if (shouldRestartAgent(result, stopping)) {
131
+ console.error(
132
+ `Agent stopped (code=${result.code ?? "null"}, signal=${result.signal ?? "none"}). Restarting in 2s...`,
133
+ );
134
+ await new Promise((resolve) => setTimeout(resolve, 2000));
135
+ }
91
136
  }
92
137
  }
93
138
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anusornneal/chat-relay",
3
- "version": "0.7.2",
3
+ "version": "0.8.0",
4
4
  "description": "Zero-checkout remote MCP agent for ChatGPT with terminal, filesystem, process, multi-user, and multi-agent support.",
5
5
  "type": "module",
6
6
  "bin": {