@anusornneal/chat-relay 0.7.2 → 0.8.1

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,201 +1,241 @@
1
- import { clearConfig, configPath, loadConfig, saveConfig } from "./config.mjs";
2
- import { login, requestJson } from "./device-login.mjs";
3
- import { remote, validateConfig } from "./remote.mjs";
4
-
5
- function parseOptions(args) {
6
- const options = {};
7
- const rest = [];
8
- for (let index = 0; index < args.length; index++) {
9
- const arg = args[index];
10
- if (arg === "--no-open") options.noOpen = true;
11
- else if (arg === "--force") options.force = true;
12
- else if (arg === "--desktop") options.desktopEnabled = true;
13
- else if (arg === "--no-desktop") options.desktopEnabled = false;
14
- else if (arg === "--relay") options.relayUrl = args[++index];
15
- else if (arg.startsWith("--relay=")) options.relayUrl = arg.slice(8);
16
- else if (arg === "--root") options.allowedRoot = args[++index];
17
- else if (arg.startsWith("--root=")) options.allowedRoot = arg.slice(7);
18
- else if (arg === "--name") options.agentName = args[++index];
19
- else if (arg.startsWith("--name=")) options.agentName = arg.slice(7);
20
- else if (arg === "--agent-id") options.agentId = args[++index];
21
- else if (arg.startsWith("--agent-id=")) options.agentId = arg.slice(11);
22
- else rest.push(arg);
23
- }
24
- return { options, rest };
25
- }
26
-
27
- function help() {
28
- console.log(`Chat Relay
29
-
30
- Usage:
31
- chat-relay remote [options] Connect this computer to ChatGPT
32
- chat-relay login [options] Sign in and register this computer
33
- chat-relay status Show account and connection status
34
- chat-relay logout Revoke this computer login
35
- chat-relay help Show this help
36
-
37
- Options:
38
- --relay <url> Relay Worker URL
39
- --root <path> Allowed filesystem root (semicolon-separated for multiple)
40
- --name <name> Computer display name
41
- --agent-id <id> Stable agent identifier
42
- --desktop Enable Windows desktop screenshot/input access
43
- --no-desktop Disable desktop access (default)
44
- --no-open Do not open the browser automatically
45
- --force Force a new login
46
-
47
- Examples:
48
- npx @anusornneal/chat-relay@latest remote
49
- npx @anusornneal/chat-relay@latest login
50
- npx @anusornneal/chat-relay@latest status
51
- `);
52
- }
53
-
54
- async function status() {
55
- const config = loadConfig();
56
- if (!config) {
57
- console.log("Chat Relay: not signed in");
58
- console.log("Access: sign-in required");
59
- console.log('Recovery: run "chat-relay login", then "chat-relay remote".');
60
- console.log("Config: " + configPath());
61
- return 1;
62
- }
63
-
64
- const me = await requestJson(config.relayUrl + "/auth/me", {
65
- headers: { authorization: "Bearer " + config.userToken },
66
- }).catch((error) => ({
67
- response: { ok: false, status: 0 },
68
- data: { error: error.message },
69
- }));
70
-
71
- if (!me.response.ok) {
72
- console.log("Chat Relay");
73
- console.log("----------");
74
- console.log("Relay: " + config.relayUrl);
75
- console.log("Device: " + config.agentName + " (" + config.agentId + ")");
76
- if (me.response.status === 401 || me.response.status === 403) {
77
- console.log("Access: sign-in expired");
78
- console.log('Recovery: run "chat-relay login --force", then "chat-relay remote".');
79
- } else {
80
- console.log("Access: relay unavailable");
81
- console.log("Recovery: retry status when the relay is reachable.");
82
- }
83
- console.log("Config: " + configPath());
84
- return 1;
85
- }
86
-
87
- const agentStatus = await requestJson(
88
- config.relayUrl + "/status?agentId=" + encodeURIComponent(config.agentId),
89
- { headers: { authorization: "Bearer " + config.userToken } },
90
- ).catch((error) => ({
91
- response: { ok: false, status: 0 },
92
- data: { error: error.message, online: false },
93
- }));
94
-
95
- const serverAgentName = agentStatus.response.ok && agentStatus.data.agentName
96
- ? String(agentStatus.data.agentName)
97
- : config.agentName;
98
- if (agentStatus.response.ok && serverAgentName !== config.agentName) {
99
- saveConfig({ ...config, agentName: serverAgentName });
100
- }
101
-
102
- const reauthorizationRequired = agentStatus.response.ok
103
- ? agentStatus.data.reauthorizationRequired === true || agentStatus.data.authorized === false
104
- : agentStatus.response.status === 401 || agentStatus.response.status === 403;
105
-
106
- console.log("Chat Relay");
107
- console.log("----------");
108
- console.log(
109
- "Account: " +
110
- (me.data.user?.name || me.data.user?.login || "signed in") +
111
- " (" + (me.data.user?.id || "unknown") + ")",
112
- );
113
- console.log("Login: " + (me.data.user?.login || "-"));
114
- console.log("Device: " + serverAgentName + " (" + config.agentId + ")");
115
- console.log("Relay: " + config.relayUrl);
116
- console.log("Access: " + (reauthorizationRequired ? "re-authorization required" : "authorized"));
117
- console.log(
118
- "Remote: " +
119
- (agentStatus.response.ok && agentStatus.data.online ? "connected" : "offline"),
120
- );
121
- if (agentStatus.response.ok) {
122
- console.log("LastSeen: " + (agentStatus.data.lastSeenAt || "-"));
123
- console.log(
124
- "Scopes: " +
125
- (Array.isArray(agentStatus.data.scopes) ? agentStatus.data.scopes.join(",") : "-"),
126
- );
127
- }
128
- console.log("Files: " + config.allowedRoots);
129
- console.log("Desktop: " + (config.desktopEnabled === true ? "enabled" : "disabled"));
130
- console.log("Config: " + configPath());
131
-
132
- if (reauthorizationRequired) {
133
- console.log('Recovery: run "chat-relay login --force", then "chat-relay remote".');
134
- return 1;
135
- }
136
- if (!agentStatus.response.ok && agentStatus.response.status === 0) {
137
- console.log("Recovery: retry status when the relay is reachable.");
138
- return 1;
139
- }
140
- return 0;
141
- }
142
- async function logout() {
143
- const config = loadConfig();
144
- if (!config) {
145
- console.log("Chat Relay: already logged out");
146
- return 0;
147
- }
148
-
149
- let revoked = false;
150
- try {
151
- const result = await requestJson(`${config.relayUrl}/auth/logout`, {
152
- method: "POST",
153
- headers: { authorization: `Bearer ${config.userToken}` },
154
- body: JSON.stringify({ agentId: config.agentId }),
155
- });
156
- revoked = result.response.ok || result.response.status === 401;
157
- if (!revoked) {
158
- throw new Error(result.data.error || `HTTP ${result.response.status}`);
159
- }
160
- } catch (error) {
161
- console.error(`Logout could not revoke the server session: ${error instanceof Error ? error.message : error}`);
162
- console.error("Local credentials were kept. Retry when the relay is reachable.");
163
- return 1;
164
- }
165
-
166
- clearConfig();
167
- console.log("Chat Relay: logged out and local credentials removed");
168
- return 0;
169
- }
170
-
171
- export async function runCli(argv = process.argv.slice(2)) {
172
- const command = (argv[0] || "remote").toLowerCase();
173
- const { options } = parseOptions(argv.slice(1));
174
-
175
- if (command === "help" || command === "--help" || command === "-h") {
176
- help();
177
- return 0;
178
- }
179
-
180
- if (command === "login") {
181
- const current = loadConfig();
182
- if (!options.force && await validateConfig(current)) {
183
- console.log(`Already signed in as ${current.user?.name || current.user?.login || "user"}.`);
184
- console.log("Use --force to sign in again.");
185
- return 0;
186
- }
187
- await login(options);
188
- return 0;
189
- }
190
-
191
- if (command === "logout") return logout();
192
- if (command === "status") return status();
193
- if (command === "remote") {
194
- await remote(options);
195
- return 0;
196
- }
197
-
198
- console.error(`Unknown command: ${command}`);
199
- help();
200
- return 1;
201
- }
1
+ import { clearConfig, configPath, loadConfig, saveConfig } from "./config.mjs";
2
+ import { login, requestJson } from "./device-login.mjs";
3
+ import { protocolCompatibility, remote, validateConfig } from "./remote.mjs";
4
+
5
+ function parseOptions(args) {
6
+ const options = {};
7
+ const rest = [];
8
+ for (let index = 0; index < args.length; index++) {
9
+ const arg = args[index];
10
+ if (arg === "--no-open") options.noOpen = true;
11
+ else if (arg === "--force") options.force = true;
12
+ else if (arg === "--desktop") options.desktopEnabled = true;
13
+ else if (arg === "--no-desktop") options.desktopEnabled = false;
14
+ else if (arg === "--relay") options.relayUrl = args[++index];
15
+ else if (arg.startsWith("--relay=")) options.relayUrl = arg.slice(8);
16
+ else if (arg === "--root") options.allowedRoot = args[++index];
17
+ else if (arg.startsWith("--root=")) options.allowedRoot = arg.slice(7);
18
+ else if (arg === "--name") options.agentName = args[++index];
19
+ else if (arg.startsWith("--name=")) options.agentName = arg.slice(7);
20
+ else if (arg === "--agent-id") options.agentId = args[++index];
21
+ else if (arg.startsWith("--agent-id=")) options.agentId = arg.slice(11);
22
+ else rest.push(arg);
23
+ }
24
+ return { options, rest };
25
+ }
26
+
27
+ function help() {
28
+ console.log(`Chat Relay
29
+
30
+ Usage:
31
+ chat-relay remote [options] Connect this computer to ChatGPT
32
+ chat-relay login [options] Sign in and register this computer
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
37
+ chat-relay logout Revoke this computer login
38
+ chat-relay help Show this help
39
+
40
+ Options:
41
+ --relay <url> Relay Worker URL
42
+ --root <path> Allowed filesystem root (semicolon-separated for multiple)
43
+ --name <name> Computer display name
44
+ --agent-id <id> Stable agent identifier
45
+ --desktop Enable Windows desktop screenshot/input access
46
+ --no-desktop Disable desktop access (default)
47
+ --no-open Do not open the browser automatically
48
+ --force Force a new login
49
+
50
+ Examples:
51
+ npx @anusornneal/chat-relay@latest remote
52
+ npx @anusornneal/chat-relay@latest login
53
+ npx @anusornneal/chat-relay@latest status
54
+ `);
55
+ }
56
+
57
+ async function status() {
58
+ const config = loadConfig();
59
+ if (!config) {
60
+ console.log("Chat Relay: not signed in");
61
+ console.log("Access: sign-in required");
62
+ console.log('Recovery: run "chat-relay login", then "chat-relay remote".');
63
+ console.log("Config: " + configPath());
64
+ return 1;
65
+ }
66
+
67
+ const me = await requestJson(config.relayUrl + "/auth/me", {
68
+ headers: { authorization: "Bearer " + config.userToken },
69
+ }).catch((error) => ({
70
+ response: { ok: false, status: 0 },
71
+ data: { error: error.message },
72
+ }));
73
+
74
+ if (!me.response.ok) {
75
+ console.log("Chat Relay");
76
+ console.log("----------");
77
+ console.log("Relay: " + config.relayUrl);
78
+ console.log("Device: " + config.agentName + " (" + config.agentId + ")");
79
+ if (me.response.status === 401 || me.response.status === 403) {
80
+ console.log("Access: sign-in expired");
81
+ console.log('Recovery: run "chat-relay login --force", then "chat-relay remote".');
82
+ } else {
83
+ console.log("Access: relay unavailable");
84
+ console.log("Recovery: retry status when the relay is reachable.");
85
+ }
86
+ console.log("Config: " + configPath());
87
+ return 1;
88
+ }
89
+
90
+ const agentStatus = await requestJson(
91
+ config.relayUrl + "/status?agentId=" + encodeURIComponent(config.agentId),
92
+ { headers: { authorization: "Bearer " + config.userToken } },
93
+ ).catch((error) => ({
94
+ response: { ok: false, status: 0 },
95
+ data: { error: error.message, online: false },
96
+ }));
97
+
98
+ const serverAgentName = agentStatus.response.ok && agentStatus.data.agentName
99
+ ? String(agentStatus.data.agentName)
100
+ : config.agentName;
101
+ if (agentStatus.response.ok && serverAgentName !== config.agentName) {
102
+ saveConfig({ ...config, agentName: serverAgentName });
103
+ }
104
+
105
+ const reauthorizationRequired = agentStatus.response.ok
106
+ ? agentStatus.data.reauthorizationRequired === true || agentStatus.data.authorized === false
107
+ : agentStatus.response.status === 401 || agentStatus.response.status === 403;
108
+
109
+ console.log("Chat Relay");
110
+ console.log("----------");
111
+ console.log(
112
+ "Account: " +
113
+ (me.data.user?.name || me.data.user?.login || "signed in") +
114
+ " (" + (me.data.user?.id || "unknown") + ")",
115
+ );
116
+ console.log("Login: " + (me.data.user?.login || "-"));
117
+ console.log("Device: " + serverAgentName + " (" + config.agentId + ")");
118
+ console.log("Relay: " + config.relayUrl);
119
+ console.log("Access: " + (reauthorizationRequired ? "re-authorization required" : "authorized"));
120
+ console.log(
121
+ "Remote: " +
122
+ (agentStatus.response.ok && agentStatus.data.online ? "connected" : "offline"),
123
+ );
124
+ if (agentStatus.response.ok) {
125
+ console.log("LastSeen: " + (agentStatus.data.lastSeenAt || "-"));
126
+ console.log(
127
+ "Scopes: " +
128
+ (Array.isArray(agentStatus.data.scopes) ? agentStatus.data.scopes.join(",") : "-"),
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
+ }
139
+ }
140
+ console.log("Files: " + config.allowedRoots);
141
+ console.log("Desktop: " + (config.desktopEnabled === true ? "enabled" : "disabled"));
142
+ console.log("Config: " + configPath());
143
+
144
+ if (reauthorizationRequired) {
145
+ console.log('Recovery: run "chat-relay login --force", then "chat-relay remote".');
146
+ return 1;
147
+ }
148
+ if (!agentStatus.response.ok && agentStatus.response.status === 0) {
149
+ console.log("Recovery: retry status when the relay is reachable.");
150
+ return 1;
151
+ }
152
+ return 0;
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
+
179
+ async function logout() {
180
+ const config = loadConfig();
181
+ if (!config) {
182
+ console.log("Chat Relay: already logged out");
183
+ return 0;
184
+ }
185
+
186
+ let revoked = false;
187
+ try {
188
+ const result = await requestJson(`${config.relayUrl}/auth/logout`, {
189
+ method: "POST",
190
+ headers: { authorization: `Bearer ${config.userToken}` },
191
+ body: JSON.stringify({ agentId: config.agentId }),
192
+ });
193
+ revoked = result.response.ok || result.response.status === 401;
194
+ if (!revoked) {
195
+ throw new Error(result.data.error || `HTTP ${result.response.status}`);
196
+ }
197
+ } catch (error) {
198
+ console.error(`Logout could not revoke the server session: ${error instanceof Error ? error.message : error}`);
199
+ console.error("Local credentials were kept. Retry when the relay is reachable.");
200
+ return 1;
201
+ }
202
+
203
+ clearConfig();
204
+ console.log("Chat Relay: logged out and local credentials removed");
205
+ return 0;
206
+ }
207
+
208
+ export async function runCli(argv = process.argv.slice(2)) {
209
+ const command = (argv[0] || "remote").toLowerCase();
210
+ const { options } = parseOptions(argv.slice(1));
211
+
212
+ if (command === "help" || command === "--help" || command === "-h") {
213
+ help();
214
+ return 0;
215
+ }
216
+
217
+ if (command === "login") {
218
+ const current = loadConfig();
219
+ if (!options.force && await validateConfig(current)) {
220
+ console.log(`Already signed in as ${current.user?.name || current.user?.login || "user"}.`);
221
+ console.log("Use --force to sign in again.");
222
+ return 0;
223
+ }
224
+ await login(options);
225
+ return 0;
226
+ }
227
+
228
+ if (command === "logout") return logout();
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");
233
+ if (command === "remote") {
234
+ await remote(options);
235
+ return 0;
236
+ }
237
+
238
+ console.error(`Unknown command: ${command}`);
239
+ help();
240
+ return 1;
241
+ }