@hemacharanpyla/infinitycli 1.0.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,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../chat.js";
package/chat.js ADDED
@@ -0,0 +1,509 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { stdin, stdout, exit } from "process";
4
+ import readline from "readline";
5
+ import os from "os";
6
+
7
+ // ─── ANSI ──────────────────────────────────────────────────
8
+ const R = "\x1b[0m", B = "\x1b[1m", D = "\x1b[2m";
9
+ const O = "\x1b[38;5;208m", BO = "\x1b[1;38;5;208m";
10
+ const P = "\x1b[35m", BP = "\x1b[1;35m";
11
+ const C = "\x1b[36m", BC = "\x1b[1;36m";
12
+ const G = "\x1b[32m", Y = "\x1b[33m";
13
+ const R2 = "\x1b[31m", GR = "\x1b[90m";
14
+ const CL = "\x1b[0K";
15
+ const CURSOR_HIDE = "\x1b[?25l", CURSOR_SHOW = "\x1b[?25h";
16
+
17
+ // ─── Config ────────────────────────────────────────────────
18
+ const API = process.env.API_BASE || "http://localhost:3000";
19
+ const MODEL = process.env.MODEL || "GPT-4o";
20
+ const USER = os.userInfo().username;
21
+
22
+ // ─── Terminal dimensions ──────────────────────────────────
23
+ const H = Math.max(24, stdout.rows || 24);
24
+ const W = Math.min(Math.max(60, (stdout.columns || 80) - 4), 120);
25
+ const CONTENT_H = H - 8; // rows available for content
26
+
27
+ // ─── Layout rows (0-indexed) ──────────────────────────────
28
+ const R_TOP = 0;
29
+ const R_TITLE = 1;
30
+ const R_SEP1 = 2;
31
+ const R_CONTENT_START = 3;
32
+ const R_SEP2 = H - 5;
33
+ const R_BLANK = H - 4;
34
+ const R_PROMPT = H - 3;
35
+ const R_SHORTCUTS = H - 2;
36
+ const R_BOTTOM = H - 1;
37
+
38
+ // ─── State ─────────────────────────────────────────────────
39
+ const state = {
40
+ messages: [],
41
+ input: "",
42
+ history: [],
43
+ histIdx: -1,
44
+ status: "initializing",
45
+ loading: false,
46
+ connected: false,
47
+ sessionId: null,
48
+ spinnerIdx: 0,
49
+ spinnerTimer: null,
50
+ view: "dashboard", // "dashboard" | "chat"
51
+ showHelp: false,
52
+ };
53
+
54
+ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
55
+
56
+ function time() {
57
+ return new Date().toLocaleTimeString("en-US", { hour12: false });
58
+ }
59
+
60
+ function fmtDur(ms) {
61
+ return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(1)}s`;
62
+ }
63
+
64
+ // ─── HTTP ──────────────────────────────────────────────────
65
+ async function api(method, path, body) {
66
+ const opts = {
67
+ method,
68
+ headers: { "Content-Type": "application/json" },
69
+ };
70
+ if (body) opts.body = JSON.stringify(body);
71
+ const res = await fetch(`${API}${path}`, opts);
72
+ if (!res.ok) {
73
+ const data = await res.json().catch(() => ({}));
74
+ throw new Error(data.error || `HTTP ${res.status}`);
75
+ }
76
+ return res.status === 204 ? null : res.json();
77
+ }
78
+
79
+ // ─── ANSI-aware length ────────────────────────────────────
80
+ const reAnsi = /\x1b\[\d*(;\d*)*m/g;
81
+ function vis(s) { return s.replace(reAnsi, ""); }
82
+
83
+ // ─── Box helpers ───────────────────────────────────────────
84
+ const BOX = {
85
+ top: `${O}╭${R}${O}${"─".repeat(W + 2)}${R}${O}╮${R}`,
86
+ sep: `${O}├${R}${O}${"─".repeat(W + 2)}${R}${O}┤${R}`,
87
+ bot: `${O}╰${R}${O}${"─".repeat(W + 2)}${R}${O}╯${R}`,
88
+ empty: `${O}│${R}${" ".repeat(W + 2)}${O}│${R}`,
89
+ line: (t) => `${O}│${R} ${vis(t).padEnd(W)} ${O}│${R}`,
90
+ lineR: (l, r) => {
91
+ const pad = Math.max(0, W - vis(l).length - vis(r).length);
92
+ return `${O}│${R} ${l}${" ".repeat(pad)}${r} ${O}│${R}`;
93
+ },
94
+ };
95
+
96
+ function center(text, total) {
97
+ const pad = Math.floor((total - text.length) / 2);
98
+ return " ".repeat(Math.max(0, pad)) + text + " ".repeat(Math.max(0, total - text.length - pad));
99
+ }
100
+
101
+ // ─── Screen builder ───────────────────────────────────────
102
+ function pos(row, col = 0) {
103
+ return `\x1b[${row + 1};${col + 1}H`;
104
+ }
105
+
106
+ function clearScreen() {
107
+ stdout.write(`\x1b[${H}S\x1b[2J\x1b[3J\x1b[H`); // scroll H lines, clear
108
+ }
109
+
110
+ const SEP = `${GR}${"─".repeat(W)}${R}`;
111
+
112
+ // ─── Dashboard content ────────────────────────────────────
113
+ function buildDashboard() {
114
+ const lines = [];
115
+ lines.push("");
116
+ lines.push(` ${BO}Welcome back ${G}${USER}${R}${BO}${R}`);
117
+ lines.push("");
118
+ lines.push(` ${D}∞${R} ${BP}Infinity CLI${R}`);
119
+ lines.push(` ${GR}Model:${R} ${BO}${MODEL}${R}`);
120
+ lines.push(` ${GR}Plan:${R} ${P}Pro${R}`);
121
+ lines.push(` ${GR}Path:${R} ${C}${process.cwd()}${R}`);
122
+ lines.push("");
123
+ lines.push(` ${GR}${SEP}${R}`);
124
+ lines.push("");
125
+ lines.push(` ${B}Tips for getting started${R}`);
126
+ lines.push(` ${O}•${R} ${G}/help${R} Show available commands`);
127
+ lines.push(` ${O}•${R} ${G}/clear${R} Clear conversation`);
128
+ lines.push(` ${O}•${R} ${G}/exit${R} Exit the session`);
129
+ lines.push(` ${O}•${R} ${G}/status${R} Show session info`);
130
+ lines.push("");
131
+ lines.push(` ${B}Recent activity${R}`);
132
+ lines.push(` ${GR} No recent activity${R}`);
133
+ lines.push("");
134
+
135
+ while (lines.length < CONTENT_H) lines.push("");
136
+ if (lines.length > CONTENT_H) lines.length = CONTENT_H;
137
+ return lines;
138
+ }
139
+
140
+ // ─── Help panel ───────────────────────────────────────────
141
+ function buildHelpPanel() {
142
+ const cmds = [
143
+ ["/help", "Show available commands"],
144
+ ["/clear", "Clear conversation"],
145
+ ["/exit", "Exit the chat session"],
146
+ ["/status", "Show session & connection info"],
147
+ ];
148
+ const sc = [
149
+ ["Enter", "Send message"],
150
+ ["Shift+Enter", "Newline in input"],
151
+ ["↑/↓", "History navigation"],
152
+ ["Ctrl+L", "Clear chat"],
153
+ ["Ctrl+U", "Clear current input"],
154
+ ["Ctrl+C", "Exit application"],
155
+ ];
156
+
157
+ const lines = [];
158
+ lines.push("");
159
+ lines.push(` ${BO}${"─".repeat(28)}${R}`);
160
+ lines.push(` ${B}Commands${R}`);
161
+ for (const [k, v] of cmds) {
162
+ lines.push(` ${O}●${R} ${G}${k}${R} ${GR}${v}${R}`);
163
+ }
164
+ lines.push("");
165
+ lines.push(` ${B}Shortcuts${R}`);
166
+ for (const [k, v] of sc) {
167
+ lines.push(` ${O}●${R} ${BC}${k}${R} ${GR}${v}${R}`);
168
+ }
169
+ lines.push("");
170
+ lines.push(` ${GR}Press ${O}?${R}${GR} again or type a command to close${R}`);
171
+ lines.push("");
172
+
173
+ while (lines.length < CONTENT_H) lines.push("");
174
+ if (lines.length > CONTENT_H) lines.length = CONTENT_H;
175
+ return lines;
176
+ }
177
+
178
+ // ─── Chat content ─────────────────────────────────────────
179
+ function buildChat() {
180
+ const entries = [];
181
+
182
+ for (const msg of state.messages) {
183
+ const role = msg.role === "user" ? `${BC}You${R}` : `${BP}Infinity${R}`;
184
+ entries.push(` ${role} (${GR}${msg.ts}${R})`);
185
+ for (const line of msg.content.split("\n")) {
186
+ entries.push(` ${line}`);
187
+ }
188
+ if (msg.meta) {
189
+ entries.push(` ${GR}╰ ${msg.meta}${R}`);
190
+ }
191
+ entries.push("");
192
+ }
193
+
194
+ // Trim from front if too many lines
195
+ while (entries.length > CONTENT_H) {
196
+ const cut = entries.indexOf("") + 1;
197
+ if (cut > 0 && cut < entries.length) {
198
+ entries.splice(0, cut);
199
+ } else {
200
+ entries.splice(0, 2);
201
+ }
202
+ }
203
+
204
+ while (entries.length < CONTENT_H) entries.push("");
205
+ if (entries.length > CONTENT_H) entries.length = CONTENT_H;
206
+ return entries;
207
+ }
208
+
209
+ // ─── Full render ──────────────────────────────────────────
210
+ function render(focusInput = true) {
211
+ const content = state.showHelp ? buildHelpPanel() : (state.view === "dashboard" ? buildDashboard() : buildChat());
212
+ const title = `${BO}Infinity CLI${R} ${GR}v1.0${R}`;
213
+
214
+ const statusIcon = state.loading ? `${Y}●${R}` : state.connected ? `${G}●${R}` : `${R2}○${R}`;
215
+ const statusRight = `${GR}${MODEL}${R} ${statusIcon} ${GR}${state.status}${R}`;
216
+ const titleLine = `${" ".repeat(1)}${title}${" ".repeat(Math.max(0, W - vis(title).length - vis(statusRight).length))}${statusRight}`;
217
+
218
+ let out = "";
219
+ out += pos(R_TOP) + BOX.top;
220
+ out += pos(R_TITLE) + BOX.line(titleLine);
221
+ out += pos(R_SEP1) + BOX.sep;
222
+
223
+ for (let i = 0; i < CONTENT_H; i++) {
224
+ const text = content[i] || "";
225
+ out += pos(R_CONTENT_START + i) + BOX.line(text);
226
+ }
227
+
228
+ out += pos(R_SEP2) + BOX.sep;
229
+ out += pos(R_BLANK) + BOX.empty;
230
+
231
+ const promptText = `${P}>${R} ${state.input}`;
232
+ out += pos(R_PROMPT) + BOX.line(promptText);
233
+
234
+ const shortcuts = `${GR}?${R} ${GR}for help${R} ${GR}│${R} ${GR}↑↓${R} ${GR}history${R} ${GR}│${R} ${GR}Ctrl+C${R} ${GR}exit${R}`;
235
+ out += pos(R_SHORTCUTS) + BOX.lineR(shortcuts, state.loading ? `${Y}● ${state.status}${R}` : `${GR}${state.messages.length} msgs${R}`);
236
+
237
+ out += pos(R_BOTTOM) + BOX.bot;
238
+
239
+ if (focusInput) {
240
+ const cursorCol = 3 + 2 + state.input.length; // │ > text → cursor after text
241
+ out += pos(R_PROMPT, cursorCol);
242
+ }
243
+
244
+ stdout.write(out);
245
+ }
246
+
247
+ // ─── Prompt-only update (while typing) ────────────────────
248
+ function renderPrompt() {
249
+ const promptText = `${P}>${R} ${state.input}`;
250
+ stdout.write(`${pos(R_PROMPT)}${CL}${BOX.line(promptText)}`);
251
+ const cursorCol = 3 + 2 + state.input.length;
252
+ stdout.write(pos(R_PROMPT, cursorCol));
253
+ }
254
+
255
+ // ─── Status-only update ───────────────────────────────────
256
+ function updateStatus() {
257
+ const statusIcon = state.loading ? `${Y}●${R}` : state.connected ? `${G}●${R}` : `${R2}○${R}`;
258
+ const statusRight = `${GR}${MODEL}${R} ${statusIcon} ${GR}${state.status}${R}`;
259
+ const title = `${BO}Infinity CLI${R} ${GR}v1.0${R}`;
260
+ const titleLine = `${" ".repeat(1)}${title}${" ".repeat(Math.max(0, W - vis(title).length - vis(statusRight).length))}${statusRight}`;
261
+ stdout.write(`${pos(R_TITLE)}${CL}${BOX.line(titleLine)}`);
262
+ }
263
+
264
+ // ─── Spinner ───────────────────────────────────────────────
265
+ function startSpinner() {
266
+ state.spinnerIdx = 0;
267
+ const tick = () => {
268
+ if (!state.loading) return;
269
+ state.spinnerIdx = (state.spinnerIdx + 1) % SPINNER.length;
270
+ updateStatus();
271
+ state.spinnerTimer = setTimeout(tick, 100);
272
+ };
273
+ state.spinnerTimer = setTimeout(tick, 100);
274
+ }
275
+
276
+ function stopSpinner() {
277
+ if (state.spinnerTimer) {
278
+ clearTimeout(state.spinnerTimer);
279
+ state.spinnerTimer = null;
280
+ }
281
+ }
282
+
283
+ // ─── Add message & re-render ──────────────────────────────
284
+ function addMessage(role, content, meta) {
285
+ state.messages.push({ role, content, meta, ts: time() });
286
+ if (state.view === "dashboard") state.view = "chat";
287
+ render();
288
+ }
289
+
290
+ // ─── Input handling ───────────────────────────────────────
291
+ function handleKeypress(str, key) {
292
+ if (!key) return;
293
+
294
+ if (key.ctrl && key.name === "c") {
295
+ if (state.messages.length > 0) {
296
+ render();
297
+ stdout.write(`${pos(R_SHORTCUTS)}${CL}${BOX.lineR(`${Y}Press Ctrl+C again or type /exit to quit${R}`, "")}`);
298
+ stdout.write(pos(R_PROMPT, 4 + state.input.length));
299
+ } else {
300
+ shutdown();
301
+ }
302
+ return;
303
+ }
304
+
305
+ if (key.name === "return") {
306
+ if (state.loading) return;
307
+ if (key.shift) {
308
+ state.input += "\n";
309
+ renderPrompt();
310
+ return;
311
+ }
312
+ const line = state.input.trimEnd();
313
+ state.input = "";
314
+ if (line) {
315
+ renderPrompt();
316
+ processLine(line);
317
+ }
318
+ return;
319
+ }
320
+
321
+ if (key.name === "backspace" || key.name === "delete") {
322
+ if (state.input.length > 0) {
323
+ state.input = state.input.slice(0, -1);
324
+ renderPrompt();
325
+ }
326
+ return;
327
+ }
328
+
329
+ if (key.name === "up") {
330
+ if (state.history.length > 0 && state.histIdx < state.history.length - 1) {
331
+ state.histIdx++;
332
+ state.input = state.history[state.history.length - 1 - state.histIdx];
333
+ renderPrompt();
334
+ }
335
+ return;
336
+ }
337
+
338
+ if (key.name === "down") {
339
+ if (state.histIdx > 0) {
340
+ state.histIdx--;
341
+ state.input = state.history[state.history.length - 1 - state.histIdx];
342
+ } else {
343
+ state.histIdx = -1;
344
+ state.input = "";
345
+ }
346
+ renderPrompt();
347
+ return;
348
+ }
349
+
350
+ if (key.ctrl && key.name === "l") { cmdClear(); return; }
351
+ if (key.ctrl && key.name === "u") { state.input = ""; renderPrompt(); return; }
352
+
353
+ if (str && !key.ctrl && !key.meta) {
354
+ state.input += str;
355
+ renderPrompt();
356
+ }
357
+ }
358
+
359
+ // ─── Command processing ────────────────────────────────────
360
+ async function processLine(line) {
361
+ state.history.push(line);
362
+ state.histIdx = -1;
363
+
364
+ if (line === "?") {
365
+ state.showHelp = !state.showHelp;
366
+ render();
367
+ return;
368
+ }
369
+
370
+ state.showHelp = false;
371
+
372
+ if (line.startsWith("/")) {
373
+ handleCommand(line);
374
+ return;
375
+ }
376
+
377
+ // Send message
378
+ addMessage("user", line);
379
+
380
+ state.loading = true;
381
+ state.status = "Thinking...";
382
+ updateStatus();
383
+ startSpinner();
384
+
385
+ const start = Date.now();
386
+
387
+ try {
388
+ if (!state.sessionId) {
389
+ state.status = "Connecting...";
390
+ updateStatus();
391
+ const sess = await api("POST", "/pages");
392
+ state.sessionId = sess.id;
393
+ state.connected = true;
394
+ state.status = "Thinking...";
395
+ updateStatus();
396
+ }
397
+
398
+ const result = await api("POST", `/pages/${state.sessionId}/messages`, { message: line });
399
+ const elapsed = Date.now() - start;
400
+ state.loading = false;
401
+ stopSpinner();
402
+ addMessage("infinity", result.response, `${fmtDur(elapsed)}`);
403
+ state.status = "Ready";
404
+ updateStatus();
405
+ } catch (err) {
406
+ const elapsed = Date.now() - start;
407
+ state.loading = false;
408
+ stopSpinner();
409
+ addMessage("infinity", `${R2}Error:${R} ${err.message}`, `failed after ${fmtDur(elapsed)}`);
410
+ state.status = `Error — ${err.message}`;
411
+ updateStatus();
412
+ }
413
+ }
414
+
415
+ function handleCommand(line) {
416
+ const cmd = line.split(/\s+/)[0].toLowerCase();
417
+ switch (cmd) {
418
+ case "/help": case "/h": case "/?":
419
+ cmdHelp(); break;
420
+ case "/clear": case "/cls":
421
+ cmdClear(); break;
422
+ case "/exit": case "/quit":
423
+ shutdown(); break;
424
+ case "/status":
425
+ cmdStatus(); break;
426
+ default:
427
+ render();
428
+ stdout.write(`${pos(R_CONTENT_START)}${CL}${BOX.line(`${R2}✕ unknown command:${R} ${line}`)}`);
429
+ stdout.write(`${pos(R_CONTENT_START + 1)}${CL}${BOX.line(`${GR}type ${B}/help${R}${GR} for available commands${R}`)}`);
430
+ stdout.write(pos(R_PROMPT, 4));
431
+ }
432
+ }
433
+
434
+ function cmdHelp() {
435
+ state.showHelp = !state.showHelp;
436
+ render();
437
+ }
438
+
439
+ function cmdClear() {
440
+ state.messages = [];
441
+ state.view = "dashboard";
442
+ render();
443
+ }
444
+
445
+ function cmdStatus() {
446
+ const lines = [
447
+ "",
448
+ ` ${BO}Session${R}`,
449
+ ` ${GR}ID:${R} ${state.sessionId || `${R2}none${R}`}`,
450
+ ` ${GR}messages:${R} ${state.messages.length}`,
451
+ ` ${GR}connected:${R} ${state.connected ? `${G}yes${R}` : `${R2}no${R}`}`,
452
+ ` ${GR}server:${R} ${API}`,
453
+ ` ${GR}model:${R} ${MODEL}`,
454
+ "",
455
+ ];
456
+ while (lines.length < CONTENT_H) lines.push("");
457
+ if (lines.length > CONTENT_H) lines.length = CONTENT_H;
458
+
459
+ for (let i = 0; i < CONTENT_H; i++) {
460
+ stdout.write(`${pos(R_CONTENT_START + i)}${CL}${BOX.line(lines[i])}`);
461
+ }
462
+ stdout.write(pos(R_PROMPT, 4));
463
+ }
464
+
465
+ // ─── Cleanup ───────────────────────────────────────────────
466
+ async function shutdown() {
467
+ stopSpinner();
468
+ try {
469
+ if (state.sessionId) await api("DELETE", `/pages/${state.sessionId}`);
470
+ } catch {}
471
+ stdout.write(CURSOR_SHOW);
472
+ stdout.write(`\x1b[${H + 1};1H`);
473
+ stdout.write(`\n ${GR}goodbye from${R} ${BO}Infinity CLI${R} ${GR}${"♥"}${R}\n`);
474
+ exit(0);
475
+ }
476
+
477
+ // ─── Init ──────────────────────────────────────────────────
478
+ async function init() {
479
+ stdout.write(CURSOR_HIDE);
480
+ clearScreen();
481
+ stdout.write(`\x1b]0;Infinity CLI\x07`);
482
+
483
+ readline.emitKeypressEvents(stdin);
484
+ try { if (stdin.isTTY) stdin.setRawMode(true); } catch {}
485
+ stdin.on("keypress", handleKeypress);
486
+
487
+ state.view = "dashboard";
488
+ state.status = "Connecting...";
489
+ render();
490
+
491
+ try {
492
+ const health = await api("GET", "/health");
493
+ state.connected = health.ok;
494
+ state.status = state.connected
495
+ ? `Ready — ${health.sessions} session${health.sessions !== 1 ? "s" : ""} active`
496
+ : "Server online, no browser yet";
497
+ updateStatus();
498
+ } catch {
499
+ state.connected = false;
500
+ state.status = "start the server: node server.js";
501
+ updateStatus();
502
+ }
503
+ }
504
+
505
+ init().catch((err) => {
506
+ stdout.write(CURSOR_SHOW);
507
+ console.error("Fatal:", err);
508
+ exit(1);
509
+ });
@@ -0,0 +1,11 @@
1
+ [
2
+ {
3
+ "name": "__Secure-next-auth.session-token",
4
+ "value": "your-session-token-here",
5
+ "domain": ".chatgpt.com",
6
+ "path": "/",
7
+ "httpOnly": true,
8
+ "secure": true,
9
+ "sameSite": "Lax"
10
+ }
11
+ ]
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@hemacharanpyla/infinitycli",
3
+ "version": "1.0.0",
4
+ "description": "∞ Infinity CLI — terminal AI chat with real-time response timing",
5
+ "type": "module",
6
+ "bin": {
7
+ "infinity": "bin/infinity.js"
8
+ },
9
+ "scripts": {
10
+ "server": "node server.js",
11
+ "chat": "node chat.js",
12
+ "start": "node server.js",
13
+ "infinity": "node bin/infinity.js",
14
+ "prepublishOnly": "node --check bin/infinity.js && node --check chat.js"
15
+ },
16
+ "keywords": [
17
+ "cli",
18
+ "ai",
19
+ "chat",
20
+ "terminal",
21
+ "assistant",
22
+ "chatgpt",
23
+ "puppeteer"
24
+ ],
25
+ "author": "Charan <hemacharanpyla@gmail.com>",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/hemacharanpyla/infinity-cli.git"
30
+ },
31
+ "bugs": {
32
+ "url": "https://github.com/hemacharanpyla/infinity-cli/issues"
33
+ },
34
+ "homepage": "https://github.com/hemacharanpyla/infinity-cli#readme",
35
+ "engines": {
36
+ "node": ">=18.0.0"
37
+ },
38
+ "files": [
39
+ "bin/",
40
+ "chat.js",
41
+ "server.js",
42
+ "cookies/",
43
+ "README.md"
44
+ ],
45
+ "dependencies": {
46
+ "express": "^4.22.2",
47
+ "puppeteer": "^23.11.1"
48
+ }
49
+ }
package/server.js ADDED
@@ -0,0 +1,424 @@
1
+ import express from "express";
2
+ import puppeteer from "puppeteer";
3
+ import { spawn } from "child_process";
4
+ import { randomUUID } from "crypto";
5
+ import { existsSync, readFileSync } from "fs";
6
+ import path from "path";
7
+
8
+ const app = express();
9
+ const publicPath = path.resolve("public");
10
+
11
+ app.use(express.static(publicPath));
12
+
13
+ const PORT = process.env.PORT || 3000;
14
+ const CHATGPT_URL = "https://chatgpt.com/";
15
+
16
+ const CHROME_PATH =
17
+ process.env.CHROME_PATH ||
18
+ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe";
19
+
20
+ const CHROME_DEBUG_PORT = process.env.CHROME_DEBUG_PORT || "9222";
21
+ const CHROME_USER_DATA_DIR =
22
+ process.env.CHROME_USER_DATA_DIR || "C:\\chrome-debug";
23
+ const COOKIES_PATH = process.env.COOKIES_PATH || "./cookies/chatgpt.json";
24
+
25
+ function positiveInt(value, fallback) {
26
+ const number = Number.parseInt(value, 10);
27
+ return Number.isFinite(number) && number > 0 ? number : fallback;
28
+ }
29
+
30
+ const MAX_SESSIONS = positiveInt(process.env.MAX_SESSIONS, 20);
31
+ const MAX_CONCURRENT_PAGE_CREATES = positiveInt(
32
+ process.env.MAX_CONCURRENT_PAGE_CREATES,
33
+ 3
34
+ );
35
+ const MAX_CONCURRENT_MESSAGES = positiveInt(
36
+ process.env.MAX_CONCURRENT_MESSAGES,
37
+ 5
38
+ );
39
+
40
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
41
+ const sessions = new Map();
42
+ let chromeProcess = null;
43
+ let browser = null;
44
+ let initializing = null;
45
+
46
+ app.use(express.json({ limit: "1mb" }));
47
+
48
+ class Semaphore {
49
+ constructor(max) {
50
+ this.max = max;
51
+ this.active = 0;
52
+ this.queue = [];
53
+ }
54
+
55
+ get queued() {
56
+ return this.queue.length;
57
+ }
58
+
59
+ async run(task) {
60
+ await this.acquire();
61
+ try {
62
+ return await task();
63
+ } finally {
64
+ this.release();
65
+ }
66
+ }
67
+
68
+ acquire() {
69
+ if (this.active < this.max) {
70
+ this.active += 1;
71
+ return Promise.resolve();
72
+ }
73
+ return new Promise((resolve) => {
74
+ this.queue.push(resolve);
75
+ });
76
+ }
77
+
78
+ release() {
79
+ const next = this.queue.shift();
80
+ if (next) {
81
+ next();
82
+ return;
83
+ }
84
+ this.active -= 1;
85
+ }
86
+ }
87
+
88
+ const pageCreateLimiter = new Semaphore(MAX_CONCURRENT_PAGE_CREATES);
89
+ const messageLimiter = new Semaphore(MAX_CONCURRENT_MESSAGES);
90
+
91
+ function loadCookies() {
92
+ if (!existsSync(COOKIES_PATH)) {
93
+ throw new Error(`Cookies file not found at ${COOKIES_PATH}`);
94
+ }
95
+ return JSON.parse(readFileSync(COOKIES_PATH, "utf-8"));
96
+ }
97
+
98
+ function startChrome() {
99
+ if (chromeProcess) return;
100
+ chromeProcess = spawn(
101
+ CHROME_PATH,
102
+ [
103
+ `--remote-debugging-port=${CHROME_DEBUG_PORT}`,
104
+ "--headless=new",
105
+ "--disable-gpu",
106
+ "--no-sandbox",
107
+ "--disable-dev-shm-usage",
108
+ "--window-size=1440,900",
109
+ `--user-data-dir=${CHROME_USER_DATA_DIR}`,
110
+ ],
111
+ {
112
+ detached: true,
113
+ stdio: "ignore",
114
+ }
115
+ );
116
+ chromeProcess.unref();
117
+ }
118
+
119
+ async function initBrowser() {
120
+ if (browser?.connected) return browser;
121
+ if (initializing) return initializing;
122
+
123
+ initializing = (async () => {
124
+ startChrome();
125
+ await sleep(5000);
126
+
127
+ browser = await puppeteer.connect({
128
+ browserURL: `http://127.0.0.1:${CHROME_DEBUG_PORT}`,
129
+ defaultViewport: null,
130
+ });
131
+
132
+ browser.on("disconnected", () => {
133
+ browser = null;
134
+ sessions.clear();
135
+ });
136
+
137
+ return browser;
138
+ })();
139
+
140
+ try {
141
+ return await initializing;
142
+ } finally {
143
+ initializing = null;
144
+ }
145
+ }
146
+
147
+ async function preparePage(page) {
148
+ const cookies = loadCookies();
149
+ await page.setUserAgent(
150
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36"
151
+ );
152
+ await page.goto(CHATGPT_URL, {
153
+ waitUntil: "networkidle2",
154
+ timeout: 60000,
155
+ });
156
+ await page.setCookie(...cookies);
157
+ await page.reload({
158
+ waitUntil: "networkidle2",
159
+ timeout: 60000,
160
+ });
161
+ await page.waitForSelector("#prompt-textarea", {
162
+ visible: true,
163
+ timeout: 60000,
164
+ });
165
+ }
166
+
167
+ async function createSession() {
168
+ if (sessions.size >= MAX_SESSIONS) {
169
+ const error = new Error(
170
+ `Maximum sessions reached. Close an existing page or increase MAX_SESSIONS.`
171
+ );
172
+ error.status = 429;
173
+ throw error;
174
+ }
175
+
176
+ return pageCreateLimiter.run(async () => {
177
+ if (sessions.size >= MAX_SESSIONS) {
178
+ const error = new Error(
179
+ `Maximum sessions reached. Close an existing page or increase MAX_SESSIONS.`
180
+ );
181
+ error.status = 429;
182
+ throw error;
183
+ }
184
+
185
+ const activeBrowser = await initBrowser();
186
+ const page = await activeBrowser.newPage();
187
+ await preparePage(page);
188
+
189
+ const id = randomUUID();
190
+ sessions.set(id, {
191
+ id,
192
+ page,
193
+ busy: false,
194
+ createdAt: new Date().toISOString(),
195
+ lastMessageAt: null,
196
+ });
197
+
198
+ page.on("close", () => sessions.delete(id));
199
+
200
+ return sessions.get(id);
201
+ });
202
+ }
203
+
204
+ function getSession(id) {
205
+ const session = sessions.get(id);
206
+ if (!session) {
207
+ const error = new Error(`Session not found: ${id}`);
208
+ error.status = 404;
209
+ throw error;
210
+ }
211
+ return session;
212
+ }
213
+
214
+ async function waitForAssistantResponse(page, previousAssistantCount) {
215
+ await page.waitForFunction(
216
+ (count) =>
217
+ document.querySelectorAll('[data-message-author-role="assistant"]')
218
+ .length > count,
219
+ { timeout: 120000 },
220
+ previousAssistantCount
221
+ );
222
+
223
+ await page.waitForFunction(
224
+ () => {
225
+ const stopButton =
226
+ document.querySelector('[data-testid="stop-button"]') ||
227
+ document.querySelector('button[aria-label*="Stop"]');
228
+ return !stopButton;
229
+ },
230
+ { timeout: 180000 }
231
+ );
232
+
233
+ await sleep(1000);
234
+
235
+ return page.evaluate(() => {
236
+ const messages = document.querySelectorAll(
237
+ '[data-message-author-role="assistant"]'
238
+ );
239
+ return messages[messages.length - 1]?.innerText || "No response found";
240
+ });
241
+ }
242
+
243
+ async function sendMessage(session, message) {
244
+ if (session.busy) {
245
+ const error = new Error("This session is already processing a message");
246
+ error.status = 409;
247
+ throw error;
248
+ }
249
+
250
+ session.busy = true;
251
+
252
+ try {
253
+ return await messageLimiter.run(async () => {
254
+ const { page } = session;
255
+ const previousAssistantCount = await page.evaluate(
256
+ () =>
257
+ document.querySelectorAll('[data-message-author-role="assistant"]')
258
+ .length
259
+ );
260
+
261
+ await page.click("#prompt-textarea");
262
+ await page.evaluate(() => {
263
+ const element = document.querySelector("#prompt-textarea");
264
+ if (element) element.innerText = "";
265
+ });
266
+
267
+ await page.keyboard.type(message, { delay: 20 });
268
+
269
+ await page.waitForFunction(() => {
270
+ const button = document.querySelector("#composer-submit-button");
271
+ return button && !button.disabled;
272
+ });
273
+
274
+ await page.click("#composer-submit-button");
275
+
276
+ const response = await waitForAssistantResponse(
277
+ page,
278
+ previousAssistantCount
279
+ );
280
+
281
+ session.lastMessageAt = new Date().toISOString();
282
+ return response;
283
+ });
284
+ } finally {
285
+ session.busy = false;
286
+ }
287
+ }
288
+
289
+ app.get("/", (req, res) => {
290
+ res.json({
291
+ name: "ChatGPT Puppeteer API",
292
+ limits: {
293
+ maxSessions: MAX_SESSIONS,
294
+ maxConcurrentPageCreates: MAX_CONCURRENT_PAGE_CREATES,
295
+ maxConcurrentMessages: MAX_CONCURRENT_MESSAGES,
296
+ },
297
+ endpoints: {
298
+ health: "GET /health",
299
+ init: "POST /init",
300
+ newPage: "POST /pages",
301
+ sendMessage: "POST /pages/:id/messages",
302
+ listPages: "GET /pages",
303
+ closePage: "DELETE /pages/:id",
304
+ shutdown: "POST /shutdown",
305
+ },
306
+ });
307
+ });
308
+
309
+ app.get("/health", (req, res) => {
310
+ res.json({
311
+ ok: true,
312
+ browserConnected: Boolean(browser?.connected),
313
+ sessions: sessions.size,
314
+ limits: {
315
+ maxSessions: MAX_SESSIONS,
316
+ maxConcurrentPageCreates: MAX_CONCURRENT_PAGE_CREATES,
317
+ maxConcurrentMessages: MAX_CONCURRENT_MESSAGES,
318
+ },
319
+ activity: {
320
+ activePageCreates: pageCreateLimiter.active,
321
+ queuedPageCreates: pageCreateLimiter.queued,
322
+ activeMessages: messageLimiter.active,
323
+ queuedMessages: messageLimiter.queued,
324
+ },
325
+ });
326
+ });
327
+
328
+ app.post("/init", async (req, res, next) => {
329
+ try {
330
+ await initBrowser();
331
+ res.json({ ok: true, browserConnected: true });
332
+ } catch (error) {
333
+ next(error);
334
+ }
335
+ });
336
+
337
+ app.post("/pages", async (req, res, next) => {
338
+ try {
339
+ const session = await createSession();
340
+ res.status(201).json({
341
+ id: session.id,
342
+ createdAt: session.createdAt,
343
+ lastMessageAt: session.lastMessageAt,
344
+ busy: session.busy,
345
+ });
346
+ } catch (error) {
347
+ next(error);
348
+ }
349
+ });
350
+
351
+ app.get("/pages", (req, res) => {
352
+ res.json({
353
+ pages: [...sessions.values()].map((session) => ({
354
+ id: session.id,
355
+ createdAt: session.createdAt,
356
+ lastMessageAt: session.lastMessageAt,
357
+ busy: session.busy,
358
+ })),
359
+ });
360
+ });
361
+
362
+ app.post("/pages/:id/messages", async (req, res, next) => {
363
+ try {
364
+ const { message } = req.body;
365
+
366
+ if (!message || typeof message !== "string") {
367
+ return res.status(400).json({
368
+ error: "Request body must include a non-empty string `message`",
369
+ });
370
+ }
371
+
372
+ const session = getSession(req.params.id);
373
+ const response = await sendMessage(session, message);
374
+
375
+ res.json({
376
+ id: session.id,
377
+ message,
378
+ response,
379
+ });
380
+ } catch (error) {
381
+ next(error);
382
+ }
383
+ });
384
+
385
+ app.delete("/pages/:id", async (req, res, next) => {
386
+ try {
387
+ const session = getSession(req.params.id);
388
+ await session.page.close();
389
+ sessions.delete(session.id);
390
+ res.status(204).send();
391
+ } catch (error) {
392
+ next(error);
393
+ }
394
+ });
395
+
396
+ app.post("/shutdown", async (req, res, next) => {
397
+ try {
398
+ for (const session of sessions.values()) {
399
+ await session.page.close().catch(() => {});
400
+ }
401
+
402
+ sessions.clear();
403
+
404
+ if (browser?.connected) {
405
+ await browser.close();
406
+ }
407
+
408
+ browser = null;
409
+ res.json({ ok: true });
410
+ } catch (error) {
411
+ next(error);
412
+ }
413
+ });
414
+
415
+ app.use((error, req, res, next) => {
416
+ console.error(error);
417
+ res.status(error.status || 500).json({
418
+ error: error.message || "Internal server error",
419
+ });
420
+ });
421
+
422
+ app.listen(PORT, () => {
423
+ console.log(`∞ CLI Server running at http://localhost:${PORT}`);
424
+ });