@anusornneal/chat-relay 0.6.0 → 0.7.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,145 +1,201 @@
1
- import { clearConfig, configPath, loadConfig } 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 === "--relay") options.relayUrl = args[++index];
13
- else if (arg.startsWith("--relay=")) options.relayUrl = arg.slice(8);
14
- else if (arg === "--root") options.allowedRoot = args[++index];
15
- else if (arg.startsWith("--root=")) options.allowedRoot = arg.slice(7);
16
- else if (arg === "--name") options.agentName = args[++index];
17
- else if (arg.startsWith("--name=")) options.agentName = arg.slice(7);
18
- else if (arg === "--agent-id") options.agentId = args[++index];
19
- else if (arg.startsWith("--agent-id=")) options.agentId = arg.slice(11);
20
- else rest.push(arg);
21
- }
22
- return { options, rest };
23
- }
24
-
25
- function help() {
26
- console.log(`Chat Relay
27
-
28
- Usage:
29
- chat-relay remote [options] Connect this computer to ChatGPT
30
- chat-relay login [options] Sign in and register this computer
31
- chat-relay status Show account and connection status
32
- chat-relay logout Revoke this computer login
33
- chat-relay help Show this help
34
-
35
- Options:
36
- --relay <url> Relay Worker URL
37
- --root <path> Allowed filesystem root (semicolon-separated for multiple)
38
- --name <name> Computer display name
39
- --agent-id <id> Stable agent identifier
40
- --no-open Do not open the browser automatically
41
- --force Force a new login
42
-
43
- Examples:
44
- npx @anusornneal/chat-relay@latest remote
45
- npx @anusornneal/chat-relay@latest login
46
- npx @anusornneal/chat-relay@latest status
47
- `);
48
- }
49
-
50
- async function status() {
51
- const config = loadConfig();
52
- if (!config) {
53
- console.log("Chat Relay: not signed in");
54
- console.log(`Config: ${configPath()}`);
55
- return 1;
56
- }
57
-
58
- const me = await requestJson(`${config.relayUrl}/auth/me`, {
59
- headers: { authorization: `Bearer ${config.userToken}` },
60
- }).catch((error) => ({ response: { ok: false, status: 0 }, data: { error: error.message } }));
61
-
62
- if (!me.response.ok) {
63
- console.log("Chat Relay: login expired or unavailable");
64
- console.log(`Relay: ${config.relayUrl}`);
65
- console.log(`Agent: ${config.agentName} (${config.agentId})`);
66
- console.log(`Config: ${configPath()}`);
67
- return 1;
68
- }
69
-
70
- const agentStatus = await requestJson(
71
- `${config.relayUrl}/status?agentId=${encodeURIComponent(config.agentId)}`,
72
- { headers: { authorization: `Bearer ${config.userToken}` } },
73
- ).catch(() => ({ response: { ok: false }, data: { online: false } }));
74
-
75
- console.log("Chat Relay");
76
- console.log("----------");
77
- console.log(`Account: ${me.data.user?.name || me.data.user?.login || "signed in"}`);
78
- console.log(`Login: ${me.data.user?.login || "-"}`);
79
- console.log(`Agent: ${config.agentName} (${config.agentId})`);
80
- console.log(`Remote: ${agentStatus.response.ok && agentStatus.data.online ? "connected" : "offline"}`);
81
- console.log(`Files: ${config.allowedRoots}`);
82
- console.log(`Config: ${configPath()}`);
83
- return 0;
84
- }
85
-
86
- async function logout() {
87
- const config = loadConfig();
88
- if (!config) {
89
- console.log("Chat Relay: already logged out");
90
- return 0;
91
- }
92
-
93
- let revoked = false;
94
- try {
95
- const result = await requestJson(`${config.relayUrl}/auth/logout`, {
96
- method: "POST",
97
- headers: { authorization: `Bearer ${config.userToken}` },
98
- body: JSON.stringify({ agentId: config.agentId }),
99
- });
100
- revoked = result.response.ok || result.response.status === 401;
101
- if (!revoked) {
102
- throw new Error(result.data.error || `HTTP ${result.response.status}`);
103
- }
104
- } catch (error) {
105
- console.error(`Logout could not revoke the server session: ${error instanceof Error ? error.message : error}`);
106
- console.error("Local credentials were kept. Retry when the relay is reachable.");
107
- return 1;
108
- }
109
-
110
- clearConfig();
111
- console.log("Chat Relay: logged out and local credentials removed");
112
- return 0;
113
- }
114
-
115
- export async function runCli(argv = process.argv.slice(2)) {
116
- const command = (argv[0] || "remote").toLowerCase();
117
- const { options } = parseOptions(argv.slice(1));
118
-
119
- if (command === "help" || command === "--help" || command === "-h") {
120
- help();
121
- return 0;
122
- }
123
-
124
- if (command === "login") {
125
- const current = loadConfig();
126
- if (!options.force && await validateConfig(current)) {
127
- console.log(`Already signed in as ${current.user?.name || current.user?.login || "user"}.`);
128
- console.log("Use --force to sign in again.");
129
- return 0;
130
- }
131
- await login(options);
132
- return 0;
133
- }
134
-
135
- if (command === "logout") return logout();
136
- if (command === "status") return status();
137
- if (command === "remote") {
138
- await remote(options);
139
- return 0;
140
- }
141
-
142
- console.error(`Unknown command: ${command}`);
143
- help();
144
- return 1;
145
- }
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
+ }
package/cli/remote.mjs CHANGED
@@ -1,88 +1,94 @@
1
- import { spawn } from "node:child_process";
2
- import { fileURLToPath } from "node:url";
3
- import path from "node:path";
4
- import { loadConfig, saveConfig } from "./config.mjs";
5
- import { login, requestJson } from "./device-login.mjs";
6
-
7
- const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
- let stopping = false;
9
-
10
- async function validateConfig(config) {
11
- if (!config?.relayUrl || !config?.userToken || !config?.agentToken || !config?.agentId) return false;
12
- const { response } = await requestJson(`${config.relayUrl}/auth/me`, {
13
- headers: { authorization: `Bearer ${config.userToken}` },
14
- }).catch(() => ({ response: { ok: false } }));
15
- return Boolean(response.ok);
16
- }
17
-
18
- function printSummary(config) {
19
- console.log("");
20
- console.log("Chat Relay Remote");
21
- console.log("-----------------");
22
- console.log(`Account: ${config.user?.name || config.user?.login || "signed in"}`);
23
- console.log(`Relay: ${config.relayUrl}`);
24
- console.log(`Agent: ${config.agentName} (${config.agentId})`);
25
- console.log(`Files: ${config.allowedRoots}`);
26
- console.log(`Terminal: ${config.terminalEnabled === false ? "disabled" : "enabled"}`);
27
- console.log("");
28
- }
29
-
30
- function spawnAgent(config) {
31
- const agentPath = path.join(packageRoot, "agent", "local-agent.mjs");
32
- const cwd = String(config.allowedRoots || process.cwd()).split(";")[0] || process.cwd();
33
- return spawn(process.execPath, [agentPath], {
34
- cwd,
35
- env: {
36
- ...process.env,
37
- RELAY_URL: config.relayUrl,
38
- AGENT_ID: config.agentId,
39
- AGENT_NAME: config.agentName,
40
- AGENT_TOKEN: config.agentToken,
41
- TERMINAL_ENABLED: config.terminalEnabled === false ? "0" : "1",
42
- ALLOWED_ROOTS: config.allowedRoots,
43
- },
44
- stdio: "inherit",
45
- windowsHide: false,
46
- });
47
- }
48
-
49
- export async function remote(options = {}) {
50
- let config = loadConfig();
51
- if (!(await validateConfig(config))) {
52
- console.log("No valid Chat Relay login found. Starting sign in...");
53
- config = await login(options);
54
- }
55
-
56
- if (options.allowedRoot && options.allowedRoot !== config.allowedRoots) {
57
- config = { ...config, allowedRoots: options.allowedRoot };
58
- saveConfig(config);
59
- }
60
-
61
- printSummary(config);
62
-
63
- while (!stopping) {
64
- const child = spawnAgent(config);
65
-
66
- const stop = () => {
67
- stopping = true;
68
- if (!child.killed) child.kill();
69
- };
70
- process.once("SIGINT", stop);
71
- process.once("SIGTERM", stop);
72
-
73
- const result = await new Promise((resolve) => {
74
- child.once("exit", (code, signal) => resolve({ code, signal }));
75
- });
76
-
77
- process.removeListener("SIGINT", stop);
78
- process.removeListener("SIGTERM", stop);
79
- if (stopping) break;
80
-
81
- console.error(
82
- `Agent stopped (code=${result.code ?? "null"}, signal=${result.signal ?? "none"}). Restarting in 2s...`,
83
- );
84
- await new Promise((resolve) => setTimeout(resolve, 2000));
85
- }
86
- }
87
-
88
- export { validateConfig };
1
+ import { spawn } from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+ import path from "node:path";
4
+ import { loadConfig, saveConfig } from "./config.mjs";
5
+ import { login, requestJson } from "./device-login.mjs";
6
+
7
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ let stopping = false;
9
+
10
+ async function validateConfig(config) {
11
+ if (!config?.relayUrl || !config?.userToken || !config?.agentToken || !config?.agentId) return false;
12
+ const { response } = await requestJson(`${config.relayUrl}/auth/me`, {
13
+ headers: { authorization: `Bearer ${config.userToken}` },
14
+ }).catch(() => ({ response: { ok: false } }));
15
+ return Boolean(response.ok);
16
+ }
17
+
18
+ function printSummary(config) {
19
+ console.log("");
20
+ console.log("Chat Relay Remote");
21
+ console.log("-----------------");
22
+ console.log(`Account: ${config.user?.name || config.user?.login || "signed in"}`);
23
+ console.log(`Relay: ${config.relayUrl}`);
24
+ console.log(`Agent: ${config.agentName} (${config.agentId})`);
25
+ console.log(`Files: ${config.allowedRoots}`);
26
+ console.log(`Terminal: ${config.terminalEnabled === false ? "disabled" : "enabled"}`);
27
+ console.log("Desktop: " + (config.desktopEnabled === true ? "enabled" : "disabled"));
28
+ console.log("");
29
+ }
30
+
31
+ function spawnAgent(config) {
32
+ const agentPath = path.join(packageRoot, "agent", "local-agent.mjs");
33
+ const cwd = String(config.allowedRoots || process.cwd()).split(";")[0] || process.cwd();
34
+ return spawn(process.execPath, [agentPath], {
35
+ cwd,
36
+ env: {
37
+ ...process.env,
38
+ RELAY_URL: config.relayUrl,
39
+ AGENT_ID: config.agentId,
40
+ AGENT_NAME: config.agentName,
41
+ AGENT_TOKEN: config.agentToken,
42
+ TERMINAL_ENABLED: config.terminalEnabled === false ? "0" : "1",
43
+ DESKTOP_ENABLED: config.desktopEnabled === true ? "1" : "0",
44
+ ALLOWED_ROOTS: config.allowedRoots,
45
+ },
46
+ stdio: "inherit",
47
+ windowsHide: false,
48
+ });
49
+ }
50
+
51
+ export async function remote(options = {}) {
52
+ let config = loadConfig();
53
+ if (!(await validateConfig(config))) {
54
+ console.log("No valid Chat Relay login found. Starting sign in...");
55
+ config = await login(options);
56
+ }
57
+
58
+ if (options.allowedRoot && options.allowedRoot !== config.allowedRoots) {
59
+ config = { ...config, allowedRoots: options.allowedRoot };
60
+ saveConfig(config);
61
+ }
62
+ if (options.desktopEnabled !== undefined && Boolean(options.desktopEnabled) !== Boolean(config.desktopEnabled)) {
63
+ config = { ...config, desktopEnabled: Boolean(options.desktopEnabled) };
64
+ saveConfig(config);
65
+ }
66
+
67
+ printSummary(config);
68
+
69
+ while (!stopping) {
70
+ const child = spawnAgent(config);
71
+
72
+ const stop = () => {
73
+ stopping = true;
74
+ if (!child.killed) child.kill();
75
+ };
76
+ process.once("SIGINT", stop);
77
+ process.once("SIGTERM", stop);
78
+
79
+ const result = await new Promise((resolve) => {
80
+ child.once("exit", (code, signal) => resolve({ code, signal }));
81
+ });
82
+
83
+ process.removeListener("SIGINT", stop);
84
+ process.removeListener("SIGTERM", stop);
85
+ if (stopping) break;
86
+
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));
91
+ }
92
+ }
93
+
94
+ export { validateConfig };