@anusornneal/chat-relay 0.6.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 ADDED
@@ -0,0 +1,145 @@
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
+ }
package/cli/remote.mjs ADDED
@@ -0,0 +1,88 @@
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 };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@anusornneal/chat-relay",
3
+ "version": "0.6.0",
4
+ "description": "Zero-checkout remote MCP agent for ChatGPT with terminal, filesystem, process, multi-user, and multi-agent support.",
5
+ "type": "module",
6
+ "bin": {
7
+ "chat-relay": "bin/chat-relay.js"
8
+ },
9
+ "files": [
10
+ "bin/",
11
+ "cli/",
12
+ "agent/",
13
+ "README.md"
14
+ ],
15
+ "engines": {
16
+ "node": ">=20"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "scripts": {
22
+ "start": "node bin/chat-relay.js remote",
23
+ "remote": "node bin/chat-relay.js remote",
24
+ "login": "node bin/chat-relay.js login",
25
+ "logout": "node bin/chat-relay.js logout",
26
+ "status": "node bin/chat-relay.js status",
27
+ "dev": "wrangler dev",
28
+ "dev:test": "wrangler dev --local --port 8790 --var AGENT_TOKEN:test-agent --var CALLER_TOKEN=test-caller --var ADMIN_TOKEN=test-admin",
29
+ "deploy": "wrangler deploy",
30
+ "agent": "node --env-file-if-exists=.dev.vars agent/local-agent.mjs",
31
+ "admin": "node scripts/admin.mjs",
32
+ "test:terminal": "node test/mcp-terminal-smoke.mjs",
33
+ "test:full": "node test/multiuser-smoke.mjs",
34
+ "test:device-auth": "node test/device-auth-smoke.mjs",
35
+ "test:zero-checkout": "node test/zero-checkout-smoke.mjs",
36
+ "verify:publish": "node scripts/verify-publish.mjs && npx wrangler deploy --dry-run && npm audit --omit=dev",
37
+ "prepublishOnly": "npm run verify:publish",
38
+ "test:oauth": "node test/oauth-smoke.mjs"
39
+ },
40
+ "dependencies": {
41
+ "ws": "^8.22.0"
42
+ },
43
+ "devDependencies": {
44
+ "@modelcontextprotocol/server": "^2.0.0",
45
+ "agents": "^0.24.0",
46
+ "wrangler": "^4.143.0",
47
+ "zod": "^4.6.5"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/anusornNeal/chat-relay.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/anusornNeal/chat-relay/issues"
55
+ },
56
+ "homepage": "https://github.com/anusornNeal/chat-relay#readme",
57
+ "keywords": [
58
+ "chatgpt",
59
+ "mcp",
60
+ "remote-agent",
61
+ "terminal",
62
+ "cloudflare",
63
+ "cli"
64
+ ]
65
+ }