claude-relay 2.1.2 → 2.2.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.
@@ -0,0 +1,187 @@
1
+ var { createTerminal } = require("./terminal");
2
+
3
+ var MAX_TERMINALS = 10;
4
+ var SCROLLBACK_MAX = 50 * 1024; // 50 KB per terminal
5
+
6
+ /**
7
+ * Create a terminal manager for a project.
8
+ * Manages persistent PTY sessions with scrollback buffering.
9
+ * opts: { cwd, send, sendTo }
10
+ */
11
+ function createTerminalManager(opts) {
12
+ var cwd = opts.cwd;
13
+ var send = opts.send;
14
+ var sendTo = opts.sendTo;
15
+
16
+ var nextId = 1;
17
+ var terminals = new Map(); // id -> terminal session
18
+
19
+ function create(cols, rows) {
20
+ if (terminals.size >= MAX_TERMINALS) return null;
21
+
22
+ var pty = createTerminal(cwd, cols, rows);
23
+ if (!pty) return null;
24
+
25
+ var id = nextId++;
26
+ var session = {
27
+ id: id,
28
+ pty: pty,
29
+ scrollback: [],
30
+ scrollbackSize: 0,
31
+ cols: cols || 80,
32
+ rows: rows || 24,
33
+ title: "Terminal " + id,
34
+ exited: false,
35
+ exitCode: null,
36
+ subscribers: new Set(),
37
+ };
38
+
39
+ pty.onData(function (data) {
40
+ // Buffer scrollback
41
+ session.scrollback.push(data);
42
+ session.scrollbackSize += data.length;
43
+ while (session.scrollbackSize > SCROLLBACK_MAX && session.scrollback.length > 1) {
44
+ session.scrollbackSize -= session.scrollback[0].length;
45
+ session.scrollback.shift();
46
+ }
47
+
48
+ // Broadcast to subscribers
49
+ var msg = JSON.stringify({ type: "term_output", id: id, data: data });
50
+ for (var ws of session.subscribers) {
51
+ if (ws.readyState === 1) ws.send(msg);
52
+ }
53
+ });
54
+
55
+ pty.onExit(function (e) {
56
+ session.exited = true;
57
+ session.exitCode = e && e.exitCode != null ? e.exitCode : null;
58
+ session.pty = null;
59
+
60
+ var msg = JSON.stringify({ type: "term_exited", id: id });
61
+ for (var ws of session.subscribers) {
62
+ if (ws.readyState === 1) ws.send(msg);
63
+ }
64
+
65
+ // Broadcast updated list
66
+ send({ type: "term_list", terminals: list() });
67
+ });
68
+
69
+ terminals.set(id, session);
70
+ return session;
71
+ }
72
+
73
+ function attach(id, ws) {
74
+ var session = terminals.get(id);
75
+ if (!session) return false;
76
+
77
+ session.subscribers.add(ws);
78
+
79
+ // Replay scrollback
80
+ if (session.scrollback.length > 0) {
81
+ var replay = session.scrollback.join("");
82
+ sendTo(ws, { type: "term_output", id: id, data: replay });
83
+ }
84
+
85
+ // If already exited, notify
86
+ if (session.exited) {
87
+ sendTo(ws, { type: "term_exited", id: id });
88
+ }
89
+
90
+ return true;
91
+ }
92
+
93
+ function detach(id, ws) {
94
+ var session = terminals.get(id);
95
+ if (!session) return;
96
+ session.subscribers.delete(ws);
97
+ }
98
+
99
+ function detachAll(ws) {
100
+ for (var session of terminals.values()) {
101
+ session.subscribers.delete(ws);
102
+ }
103
+ }
104
+
105
+ function write(id, data) {
106
+ var session = terminals.get(id);
107
+ if (session && session.pty) {
108
+ session.pty.write(data);
109
+ }
110
+ }
111
+
112
+ function resize(id, cols, rows) {
113
+ var session = terminals.get(id);
114
+ if (!session || !session.pty) return;
115
+ if (cols > 0 && rows > 0) {
116
+ try {
117
+ session.pty.resize(cols, rows);
118
+ session.cols = cols;
119
+ session.rows = rows;
120
+ } catch (e) {}
121
+ }
122
+ }
123
+
124
+ function close(id) {
125
+ var session = terminals.get(id);
126
+ if (!session) return;
127
+
128
+ if (session.pty) {
129
+ try { session.pty.kill(); } catch (e) {}
130
+ session.pty = null;
131
+ }
132
+
133
+ // Notify subscribers
134
+ var msg = JSON.stringify({ type: "term_closed", id: id });
135
+ for (var ws of session.subscribers) {
136
+ if (ws.readyState === 1) ws.send(msg);
137
+ }
138
+
139
+ terminals.delete(id);
140
+
141
+ // Reset counter when all terminals are closed
142
+ if (terminals.size === 0) nextId = 1;
143
+ }
144
+
145
+ function rename(id, title) {
146
+ var session = terminals.get(id);
147
+ if (!session) return;
148
+ session.title = String(title).substring(0, 50);
149
+ }
150
+
151
+ function list() {
152
+ var result = [];
153
+ for (var session of terminals.values()) {
154
+ result.push({
155
+ id: session.id,
156
+ title: session.title,
157
+ exited: session.exited,
158
+ });
159
+ }
160
+ return result;
161
+ }
162
+
163
+ function destroyAll() {
164
+ for (var session of terminals.values()) {
165
+ if (session.pty) {
166
+ try { session.pty.kill(); } catch (e) {}
167
+ session.pty = null;
168
+ }
169
+ }
170
+ terminals.clear();
171
+ }
172
+
173
+ return {
174
+ create: create,
175
+ attach: attach,
176
+ detach: detach,
177
+ detachAll: detachAll,
178
+ write: write,
179
+ resize: resize,
180
+ close: close,
181
+ rename: rename,
182
+ list: list,
183
+ destroyAll: destroyAll,
184
+ };
185
+ }
186
+
187
+ module.exports = { createTerminalManager: createTerminalManager };
package/lib/terminal.js CHANGED
@@ -5,14 +5,14 @@ try {
5
5
  pty = null;
6
6
  }
7
7
 
8
- function createTerminal(cwd) {
8
+ function createTerminal(cwd, cols, rows) {
9
9
  if (!pty) return null;
10
10
 
11
11
  var shell = process.env.SHELL || "/bin/bash";
12
12
  var term = pty.spawn(shell, [], {
13
13
  name: "xterm-256color",
14
- cols: 80,
15
- rows: 24,
14
+ cols: cols || 80,
15
+ rows: rows || 24,
16
16
  cwd: cwd,
17
17
  env: Object.assign({}, process.env, { TERM: "xterm-256color" }),
18
18
  });
package/lib/usage.js ADDED
@@ -0,0 +1,90 @@
1
+ var os = require("os");
2
+ var path = require("path");
3
+ var fs = require("fs");
4
+ var { execFileSync } = require("child_process");
5
+ var https = require("https");
6
+
7
+ var BASE_API_URL = "https://api.anthropic.com";
8
+
9
+ function readOAuthToken() {
10
+ // Priority 1: env var override
11
+ if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
12
+ return process.env.CLAUDE_CODE_OAUTH_TOKEN;
13
+ }
14
+
15
+ // Priority 2: macOS keychain
16
+ if (process.platform === "darwin") {
17
+ try {
18
+ var user = os.userInfo().username;
19
+ var data = execFileSync("security", [
20
+ "find-generic-password", "-a", user, "-w", "-s", "Claude Code-credentials"
21
+ ], { encoding: "utf8", timeout: 5000 }).trim();
22
+ if (data) {
23
+ var parsed = JSON.parse(data);
24
+ if (parsed.claudeAiOauth && parsed.claudeAiOauth.accessToken) {
25
+ return parsed.claudeAiOauth.accessToken;
26
+ }
27
+ }
28
+ } catch (e) { /* fall through */ }
29
+ }
30
+
31
+ // Priority 3: plaintext credentials file
32
+ var configDir = process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
33
+ var credFile = path.join(configDir, ".credentials.json");
34
+ try {
35
+ var content = fs.readFileSync(credFile, "utf8");
36
+ var parsed = JSON.parse(content);
37
+ if (parsed.claudeAiOauth && parsed.claudeAiOauth.accessToken) {
38
+ return parsed.claudeAiOauth.accessToken;
39
+ }
40
+ } catch (e) { /* fall through */ }
41
+
42
+ return null;
43
+ }
44
+
45
+ function fetchUsageData() {
46
+ return new Promise(function (resolve, reject) {
47
+ var token = readOAuthToken();
48
+ if (!token) {
49
+ reject(new Error("No OAuth token available"));
50
+ return;
51
+ }
52
+
53
+ var url = new URL(BASE_API_URL + "/api/oauth/usage");
54
+ var options = {
55
+ hostname: url.hostname,
56
+ port: url.port || 443,
57
+ path: url.pathname,
58
+ method: "GET",
59
+ headers: {
60
+ "Content-Type": "application/json",
61
+ "Authorization": "Bearer " + token,
62
+ "anthropic-beta": "oauth-2025-04-20",
63
+ "User-Agent": "claude-code/2.0.0",
64
+ },
65
+ timeout: 5000,
66
+ };
67
+
68
+ var req = https.request(options, function (res) {
69
+ var body = "";
70
+ res.on("data", function (chunk) { body += chunk; });
71
+ res.on("end", function () {
72
+ if (res.statusCode !== 200) {
73
+ reject(new Error("Usage API returned " + res.statusCode));
74
+ return;
75
+ }
76
+ try {
77
+ resolve(JSON.parse(body));
78
+ } catch (e) {
79
+ reject(new Error("Invalid JSON from usage API"));
80
+ }
81
+ });
82
+ });
83
+
84
+ req.on("error", function (err) { reject(err); });
85
+ req.on("timeout", function () { req.destroy(); reject(new Error("Usage API timeout")); });
86
+ req.end();
87
+ });
88
+ }
89
+
90
+ module.exports = { fetchUsageData: fetchUsageData };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-relay",
3
- "version": "2.1.2",
3
+ "version": "2.2.0",
4
4
  "description": "Web UI for Claude Code. Any device. Push notifications.",
5
5
  "bin": {
6
6
  "claude-relay": "./bin/cli.js"