@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/README.md +230 -0
- package/agent/file-manager.mjs +265 -0
- package/agent/local-agent.mjs +157 -0
- package/agent/process-manager.mjs +69 -0
- package/agent/terminal-manager.mjs +229 -0
- package/bin/chat-relay.js +5 -0
- package/cli/config.mjs +113 -0
- package/cli/device-login.mjs +168 -0
- package/cli/index.mjs +145 -0
- package/cli/remote.mjs +88 -0
- package/package.json +65 -0
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
|
+
}
|