@steipete/oracle 0.4.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.
Files changed (168) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +129 -0
  3. package/assets-oracle-icon.png +0 -0
  4. package/dist/bin/oracle-cli.js +954 -0
  5. package/dist/bin/oracle-mcp.js +6 -0
  6. package/dist/bin/oracle.js +683 -0
  7. package/dist/markdansi/types/index.js +4 -0
  8. package/dist/oracle/bin/oracle-cli.js +472 -0
  9. package/dist/oracle/src/browser/actions/assistantResponse.js +471 -0
  10. package/dist/oracle/src/browser/actions/attachments.js +82 -0
  11. package/dist/oracle/src/browser/actions/modelSelection.js +190 -0
  12. package/dist/oracle/src/browser/actions/navigation.js +75 -0
  13. package/dist/oracle/src/browser/actions/promptComposer.js +167 -0
  14. package/dist/oracle/src/browser/chromeLifecycle.js +104 -0
  15. package/dist/oracle/src/browser/config.js +33 -0
  16. package/dist/oracle/src/browser/constants.js +40 -0
  17. package/dist/oracle/src/browser/cookies.js +210 -0
  18. package/dist/oracle/src/browser/domDebug.js +36 -0
  19. package/dist/oracle/src/browser/index.js +331 -0
  20. package/dist/oracle/src/browser/pageActions.js +5 -0
  21. package/dist/oracle/src/browser/prompt.js +88 -0
  22. package/dist/oracle/src/browser/promptSummary.js +20 -0
  23. package/dist/oracle/src/browser/sessionRunner.js +80 -0
  24. package/dist/oracle/src/browser/types.js +1 -0
  25. package/dist/oracle/src/browser/utils.js +62 -0
  26. package/dist/oracle/src/browserMode.js +1 -0
  27. package/dist/oracle/src/cli/browserConfig.js +44 -0
  28. package/dist/oracle/src/cli/dryRun.js +59 -0
  29. package/dist/oracle/src/cli/engine.js +17 -0
  30. package/dist/oracle/src/cli/errorUtils.js +9 -0
  31. package/dist/oracle/src/cli/help.js +70 -0
  32. package/dist/oracle/src/cli/markdownRenderer.js +15 -0
  33. package/dist/oracle/src/cli/options.js +103 -0
  34. package/dist/oracle/src/cli/promptRequirement.js +14 -0
  35. package/dist/oracle/src/cli/rootAlias.js +30 -0
  36. package/dist/oracle/src/cli/sessionCommand.js +77 -0
  37. package/dist/oracle/src/cli/sessionDisplay.js +270 -0
  38. package/dist/oracle/src/cli/sessionRunner.js +94 -0
  39. package/dist/oracle/src/heartbeat.js +43 -0
  40. package/dist/oracle/src/oracle/client.js +48 -0
  41. package/dist/oracle/src/oracle/config.js +29 -0
  42. package/dist/oracle/src/oracle/errors.js +101 -0
  43. package/dist/oracle/src/oracle/files.js +220 -0
  44. package/dist/oracle/src/oracle/format.js +33 -0
  45. package/dist/oracle/src/oracle/fsAdapter.js +7 -0
  46. package/dist/oracle/src/oracle/oscProgress.js +60 -0
  47. package/dist/oracle/src/oracle/request.js +48 -0
  48. package/dist/oracle/src/oracle/run.js +444 -0
  49. package/dist/oracle/src/oracle/tokenStats.js +39 -0
  50. package/dist/oracle/src/oracle/types.js +1 -0
  51. package/dist/oracle/src/oracle.js +9 -0
  52. package/dist/oracle/src/sessionManager.js +205 -0
  53. package/dist/oracle/src/version.js +39 -0
  54. package/dist/scripts/browser-tools.js +536 -0
  55. package/dist/scripts/check.js +21 -0
  56. package/dist/scripts/chrome/browser-tools.js +295 -0
  57. package/dist/scripts/run-cli.js +14 -0
  58. package/dist/src/browser/actions/assistantResponse.js +555 -0
  59. package/dist/src/browser/actions/attachments.js +82 -0
  60. package/dist/src/browser/actions/modelSelection.js +300 -0
  61. package/dist/src/browser/actions/navigation.js +175 -0
  62. package/dist/src/browser/actions/promptComposer.js +167 -0
  63. package/dist/src/browser/actions/remoteFileTransfer.js +154 -0
  64. package/dist/src/browser/chromeCookies.js +274 -0
  65. package/dist/src/browser/chromeLifecycle.js +107 -0
  66. package/dist/src/browser/config.js +49 -0
  67. package/dist/src/browser/constants.js +42 -0
  68. package/dist/src/browser/cookies.js +130 -0
  69. package/dist/src/browser/domDebug.js +36 -0
  70. package/dist/src/browser/index.js +541 -0
  71. package/dist/src/browser/keytarShim.js +56 -0
  72. package/dist/src/browser/pageActions.js +5 -0
  73. package/dist/src/browser/policies.js +43 -0
  74. package/dist/src/browser/prompt.js +82 -0
  75. package/dist/src/browser/promptSummary.js +20 -0
  76. package/dist/src/browser/sessionRunner.js +96 -0
  77. package/dist/src/browser/types.js +1 -0
  78. package/dist/src/browser/utils.js +112 -0
  79. package/dist/src/browser/windowsCookies.js +218 -0
  80. package/dist/src/browserMode.js +1 -0
  81. package/dist/src/cli/browserConfig.js +193 -0
  82. package/dist/src/cli/bundleWarnings.js +9 -0
  83. package/dist/src/cli/clipboard.js +10 -0
  84. package/dist/src/cli/detach.js +11 -0
  85. package/dist/src/cli/dryRun.js +103 -0
  86. package/dist/src/cli/duplicatePromptGuard.js +14 -0
  87. package/dist/src/cli/engine.js +25 -0
  88. package/dist/src/cli/errorUtils.js +9 -0
  89. package/dist/src/cli/format.js +13 -0
  90. package/dist/src/cli/help.js +77 -0
  91. package/dist/src/cli/hiddenAliases.js +22 -0
  92. package/dist/src/cli/markdownBundle.js +17 -0
  93. package/dist/src/cli/markdownRenderer.js +97 -0
  94. package/dist/src/cli/notifier.js +300 -0
  95. package/dist/src/cli/options.js +193 -0
  96. package/dist/src/cli/oscUtils.js +20 -0
  97. package/dist/src/cli/promptRequirement.js +17 -0
  98. package/dist/src/cli/renderFlags.js +9 -0
  99. package/dist/src/cli/renderOutput.js +26 -0
  100. package/dist/src/cli/rootAlias.js +30 -0
  101. package/dist/src/cli/runOptions.js +62 -0
  102. package/dist/src/cli/sessionCommand.js +111 -0
  103. package/dist/src/cli/sessionDisplay.js +540 -0
  104. package/dist/src/cli/sessionRunner.js +419 -0
  105. package/dist/src/cli/tagline.js +258 -0
  106. package/dist/src/cli/tui/index.js +520 -0
  107. package/dist/src/cli/writeOutputPath.js +21 -0
  108. package/dist/src/config.js +27 -0
  109. package/dist/src/heartbeat.js +43 -0
  110. package/dist/src/mcp/server.js +36 -0
  111. package/dist/src/mcp/tools/consult.js +221 -0
  112. package/dist/src/mcp/tools/sessionResources.js +75 -0
  113. package/dist/src/mcp/tools/sessions.js +96 -0
  114. package/dist/src/mcp/types.js +18 -0
  115. package/dist/src/mcp/utils.js +27 -0
  116. package/dist/src/oracle/background.js +134 -0
  117. package/dist/src/oracle/claude.js +95 -0
  118. package/dist/src/oracle/client.js +87 -0
  119. package/dist/src/oracle/config.js +92 -0
  120. package/dist/src/oracle/errors.js +104 -0
  121. package/dist/src/oracle/files.js +371 -0
  122. package/dist/src/oracle/format.js +30 -0
  123. package/dist/src/oracle/fsAdapter.js +10 -0
  124. package/dist/src/oracle/gemini.js +185 -0
  125. package/dist/src/oracle/logging.js +36 -0
  126. package/dist/src/oracle/markdown.js +46 -0
  127. package/dist/src/oracle/multiModelRunner.js +164 -0
  128. package/dist/src/oracle/oscProgress.js +66 -0
  129. package/dist/src/oracle/promptAssembly.js +13 -0
  130. package/dist/src/oracle/request.js +49 -0
  131. package/dist/src/oracle/run.js +492 -0
  132. package/dist/src/oracle/runUtils.js +27 -0
  133. package/dist/src/oracle/tokenEstimate.js +37 -0
  134. package/dist/src/oracle/tokenStats.js +39 -0
  135. package/dist/src/oracle/tokenStringifier.js +24 -0
  136. package/dist/src/oracle/types.js +1 -0
  137. package/dist/src/oracle.js +12 -0
  138. package/dist/src/remote/client.js +128 -0
  139. package/dist/src/remote/server.js +294 -0
  140. package/dist/src/remote/types.js +1 -0
  141. package/dist/src/sessionManager.js +462 -0
  142. package/dist/src/sessionStore.js +56 -0
  143. package/dist/src/version.js +39 -0
  144. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  145. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  146. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  147. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  148. package/dist/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  149. package/dist/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  150. package/dist/vendor/oracle-notifier/README.md +24 -0
  151. package/dist/vendor/oracle-notifier/build-notifier.sh +93 -0
  152. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  153. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  154. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  155. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  156. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  157. package/dist/vendor/oracle-notifier/oracle-notifier/OracleNotifier.swift +45 -0
  158. package/dist/vendor/oracle-notifier/oracle-notifier/README.md +24 -0
  159. package/dist/vendor/oracle-notifier/oracle-notifier/build-notifier.sh +93 -0
  160. package/package.json +102 -0
  161. package/vendor/oracle-notifier/OracleNotifier.app/Contents/CodeResources +0 -0
  162. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Info.plist +20 -0
  163. package/vendor/oracle-notifier/OracleNotifier.app/Contents/MacOS/OracleNotifier +0 -0
  164. package/vendor/oracle-notifier/OracleNotifier.app/Contents/Resources/OracleIcon.icns +0 -0
  165. package/vendor/oracle-notifier/OracleNotifier.app/Contents/_CodeSignature/CodeResources +128 -0
  166. package/vendor/oracle-notifier/OracleNotifier.swift +45 -0
  167. package/vendor/oracle-notifier/README.md +24 -0
  168. package/vendor/oracle-notifier/build-notifier.sh +93 -0
@@ -0,0 +1,128 @@
1
+ import http from 'node:http';
2
+ import path from 'node:path';
3
+ import { readFile } from 'node:fs/promises';
4
+ export function createRemoteBrowserExecutor({ host, token }) {
5
+ // Return a drop-in replacement for runBrowserMode so the browser session runner can stay unchanged.
6
+ return async function remoteBrowserExecutor(options) {
7
+ const payload = {
8
+ prompt: options.prompt,
9
+ attachments: await serializeAttachments(options.attachments ?? []),
10
+ browserConfig: options.config ?? {},
11
+ options: {
12
+ heartbeatIntervalMs: options.heartbeatIntervalMs,
13
+ verbose: options.verbose,
14
+ },
15
+ };
16
+ const body = Buffer.from(JSON.stringify(payload));
17
+ const { hostname, port } = parseHost(host);
18
+ return new Promise((resolve, reject) => {
19
+ const req = http.request({
20
+ hostname,
21
+ port,
22
+ path: '/runs',
23
+ method: 'POST',
24
+ headers: {
25
+ 'Content-Type': 'application/json',
26
+ 'Content-Length': body.length,
27
+ ...(token ? { authorization: `Bearer ${token}` } : {}),
28
+ },
29
+ }, (res) => {
30
+ if (res.statusCode !== 200) {
31
+ collectError(res)
32
+ .then((message) => reject(new Error(message)))
33
+ .catch(reject);
34
+ return;
35
+ }
36
+ res.setEncoding('utf8');
37
+ let buffer = '';
38
+ let resolved = null;
39
+ res.on('data', (chunk) => {
40
+ buffer += chunk;
41
+ let newlineIndex = buffer.indexOf('\n');
42
+ while (newlineIndex !== -1) {
43
+ const line = buffer.slice(0, newlineIndex).trim();
44
+ buffer = buffer.slice(newlineIndex + 1);
45
+ if (line.length > 0) {
46
+ handleEvent(line, options, (result) => {
47
+ resolved = result;
48
+ }, reject);
49
+ }
50
+ newlineIndex = buffer.indexOf('\n');
51
+ }
52
+ });
53
+ res.on('end', () => {
54
+ if (resolved) {
55
+ resolve(resolved);
56
+ return;
57
+ }
58
+ reject(new Error('Remote browser run completed without a result.'));
59
+ });
60
+ });
61
+ req.on('error', reject);
62
+ req.write(body);
63
+ req.end();
64
+ });
65
+ };
66
+ }
67
+ async function serializeAttachments(attachments) {
68
+ const serialized = [];
69
+ for (const attachment of attachments) {
70
+ // Read the local file upfront so the remote host never touches the caller's filesystem.
71
+ const content = await readFile(attachment.path);
72
+ serialized.push({
73
+ fileName: path.basename(attachment.path),
74
+ displayPath: attachment.displayPath,
75
+ sizeBytes: attachment.sizeBytes,
76
+ contentBase64: content.toString('base64'),
77
+ });
78
+ }
79
+ return serialized;
80
+ }
81
+ function parseHost(input) {
82
+ const [hostname, portStr] = input.split(':');
83
+ const port = Number.parseInt(portStr ?? '', 10);
84
+ if (!hostname || Number.isNaN(port)) {
85
+ throw new Error(`Invalid remote host: ${input}`);
86
+ }
87
+ return { hostname, port };
88
+ }
89
+ function handleEvent(line, options, onResult, onError) {
90
+ let event;
91
+ try {
92
+ event = JSON.parse(line);
93
+ }
94
+ catch (error) {
95
+ onError(new Error(`Failed to parse remote event: ${error instanceof Error ? error.message : String(error)}`));
96
+ return;
97
+ }
98
+ if (event.type === 'log') {
99
+ options.log?.(event.message);
100
+ return;
101
+ }
102
+ if (event.type === 'error') {
103
+ onError(new Error(event.message));
104
+ return;
105
+ }
106
+ if (event.type === 'result') {
107
+ onResult(event.result);
108
+ }
109
+ }
110
+ function collectError(res) {
111
+ return new Promise((resolve, reject) => {
112
+ const chunks = [];
113
+ res.on('data', (chunk) => {
114
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
115
+ });
116
+ res.on('end', () => {
117
+ const raw = Buffer.concat(chunks).toString('utf8');
118
+ try {
119
+ const parsed = JSON.parse(raw);
120
+ resolve(parsed.error ?? `Remote host responded with status ${res.statusCode}`);
121
+ }
122
+ catch {
123
+ resolve(raw || `Remote host responded with status ${res.statusCode}`);
124
+ }
125
+ });
126
+ res.on('error', reject);
127
+ });
128
+ }
@@ -0,0 +1,294 @@
1
+ import http from 'node:http';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { randomBytes, randomUUID } from 'node:crypto';
5
+ import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
6
+ import chalk from 'chalk';
7
+ import { runBrowserMode } from '../browserMode.js';
8
+ import { loadChromeCookies } from '../browser/chromeCookies.js';
9
+ import { CHATGPT_URL } from '../browser/constants.js';
10
+ import { normalizeChatgptUrl } from '../browser/utils.js';
11
+ export async function createRemoteServer(options = {}, deps = {}) {
12
+ const runBrowser = deps.runBrowser ?? runBrowserMode;
13
+ const server = http.createServer();
14
+ const logger = options.logger ?? console.log;
15
+ const authToken = options.token ?? randomBytes(16).toString('hex');
16
+ const verbose = process.argv.includes('--verbose') || process.env.ORACLE_SERVE_VERBOSE === '1';
17
+ const color = process.stdout.isTTY
18
+ ? (formatter, msg) => formatter(msg)
19
+ : (_formatter, msg) => msg;
20
+ // Single-flight guard: remote Chrome can only host one run at a time, so we serialize requests.
21
+ let busy = false;
22
+ if (!process.listenerCount('unhandledRejection')) {
23
+ process.on('unhandledRejection', (reason) => {
24
+ logger(`Unhandled promise rejection in remote server: ${reason instanceof Error ? reason.message : String(reason)}`);
25
+ });
26
+ }
27
+ server.on('request', async (req, res) => {
28
+ if (req.method === 'GET' && req.url === '/status') {
29
+ logger('[serve] Health check /status');
30
+ res.writeHead(200, { 'Content-Type': 'application/json' });
31
+ res.end(JSON.stringify({ ok: true }));
32
+ return;
33
+ }
34
+ if (req.method !== 'POST' || req.url !== '/runs') {
35
+ res.statusCode = 404;
36
+ res.end();
37
+ return;
38
+ }
39
+ const authHeader = req.headers.authorization ?? '';
40
+ if (authHeader !== `Bearer ${authToken}`) {
41
+ if (verbose) {
42
+ logger(`[serve] Unauthorized /runs attempt from ${formatSocket(req)} (missing/invalid token)`);
43
+ }
44
+ res.writeHead(401, { 'Content-Type': 'application/json' });
45
+ res.end(JSON.stringify({ error: 'unauthorized' }));
46
+ return;
47
+ }
48
+ // biome-ignore lint/nursery/noUnnecessaryConditions: busy guard protects single-run host
49
+ if (busy) {
50
+ if (verbose) {
51
+ logger(`[serve] Busy: rejecting new run from ${formatSocket(req)} while another run is active`);
52
+ }
53
+ res.writeHead(409, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify({ error: 'busy' }));
55
+ return;
56
+ }
57
+ busy = true;
58
+ const runStartedAt = Date.now();
59
+ let payload = null;
60
+ try {
61
+ const body = await readRequestBody(req);
62
+ payload = JSON.parse(body);
63
+ if (payload?.browserConfig) {
64
+ payload.browserConfig.url = normalizeChatgptUrl(payload.browserConfig.url, CHATGPT_URL);
65
+ }
66
+ }
67
+ catch (_error) {
68
+ busy = false;
69
+ res.writeHead(400, { 'Content-Type': 'application/json' });
70
+ res.end(JSON.stringify({ error: 'invalid_request' }));
71
+ return;
72
+ }
73
+ res.writeHead(200, { 'Content-Type': 'application/x-ndjson' });
74
+ const runId = randomUUID();
75
+ logger(`[serve] Accepted run ${runId} from ${formatSocket(req)} (prompt ${payload?.prompt?.length ?? 0} chars)`);
76
+ // Each run gets an isolated temp dir so attachments/logs don't collide.
77
+ const runDir = await mkdtemp(path.join(os.tmpdir(), `oracle-serve-${runId}-`));
78
+ const attachmentDir = path.join(runDir, 'attachments');
79
+ await mkdir(attachmentDir, { recursive: true });
80
+ const sendEvent = (event) => {
81
+ res.write(`${JSON.stringify(event)}\n`);
82
+ };
83
+ const attachments = [];
84
+ try {
85
+ const attachmentsPayload = Array.isArray(payload.attachments) ? payload.attachments : [];
86
+ for (const [index, attachment] of attachmentsPayload.entries()) {
87
+ const safeName = sanitizeName(attachment.fileName ?? `attachment-${index + 1}`);
88
+ const filePath = path.join(attachmentDir, safeName);
89
+ await writeFile(filePath, Buffer.from(attachment.contentBase64, 'base64'));
90
+ attachments.push({
91
+ path: filePath,
92
+ displayPath: attachment.displayPath,
93
+ sizeBytes: attachment.sizeBytes,
94
+ });
95
+ }
96
+ // Reuse the existing browser logger surface so clients see the same log stream.
97
+ const automationLogger = ((message) => {
98
+ if (typeof message === 'string') {
99
+ sendEvent({ type: 'log', message });
100
+ }
101
+ });
102
+ automationLogger.verbose = Boolean(payload.options.verbose);
103
+ // Remote runs always rely on the host's own Chrome profile; ignore any inline cookie transfer.
104
+ if (payload.browserConfig) {
105
+ payload.browserConfig.inlineCookies = null;
106
+ payload.browserConfig.inlineCookiesSource = null;
107
+ payload.browserConfig.cookieSync = true;
108
+ }
109
+ const result = await runBrowser({
110
+ prompt: payload.prompt,
111
+ attachments,
112
+ config: payload.browserConfig,
113
+ log: automationLogger,
114
+ heartbeatIntervalMs: payload.options.heartbeatIntervalMs,
115
+ verbose: payload.options.verbose,
116
+ });
117
+ sendEvent({ type: 'result', result: sanitizeResult(result) });
118
+ logger(`[serve] Run ${runId} completed in ${Date.now() - runStartedAt}ms`);
119
+ }
120
+ catch (error) {
121
+ const message = error instanceof Error ? error.message : String(error);
122
+ sendEvent({ type: 'error', message });
123
+ logger(`[serve] Run ${runId} failed after ${Date.now() - runStartedAt}ms: ${message}`);
124
+ }
125
+ finally {
126
+ busy = false;
127
+ res.end();
128
+ try {
129
+ await rm(runDir, { recursive: true, force: true });
130
+ }
131
+ catch {
132
+ // ignore cleanup errors
133
+ }
134
+ }
135
+ });
136
+ await new Promise((resolve) => {
137
+ server.listen(options.port ?? 0, options.host ?? '0.0.0.0', () => resolve());
138
+ });
139
+ const address = server.address();
140
+ if (!address || typeof address === 'string') {
141
+ throw new Error('Unable to determine server address.');
142
+ }
143
+ const reachable = formatReachableAddresses(address.address, address.port);
144
+ const primary = reachable[0] ?? `${address.address}:${address.port}`;
145
+ const extras = reachable.slice(1);
146
+ const also = extras.length ? `, also [${extras.join(', ')}]` : '';
147
+ logger(color(chalk.cyanBright.bold, `Listening at ${primary}${also}`));
148
+ logger(color(chalk.yellowBright, `Access token: ${authToken}`));
149
+ return {
150
+ port: address.port,
151
+ token: authToken,
152
+ async close() {
153
+ await new Promise((resolve, reject) => {
154
+ server.close((err) => (err ? reject(err) : resolve()));
155
+ });
156
+ },
157
+ };
158
+ }
159
+ export async function serveRemote(options = {}) {
160
+ // Warm-up: ensure this host has a ChatGPT login before accepting runs.
161
+ const cookies = await loadLocalChatgptCookies(console.log, CHATGPT_URL);
162
+ if (!cookies || cookies.length === 0) {
163
+ console.log('No ChatGPT cookies detected on this host.');
164
+ console.log('Opened chatgpt.com for login. Sign in, then restart `oracle serve` to continue.');
165
+ return;
166
+ }
167
+ console.log(`Detected ${cookies.length} ChatGPT cookies on this host; runs will reuse this session.`);
168
+ const server = await createRemoteServer(options);
169
+ await new Promise((resolve) => {
170
+ const shutdown = () => {
171
+ console.log('Shutting down remote service...');
172
+ server
173
+ .close()
174
+ .catch((error) => console.error('Failed to close remote server:', error))
175
+ .finally(() => resolve());
176
+ };
177
+ process.on('SIGINT', shutdown);
178
+ process.on('SIGTERM', shutdown);
179
+ });
180
+ }
181
+ async function readRequestBody(req) {
182
+ const chunks = [];
183
+ for await (const chunk of req) {
184
+ chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
185
+ }
186
+ return Buffer.concat(chunks).toString('utf8');
187
+ }
188
+ function sanitizeName(raw) {
189
+ return raw.replace(/[^a-zA-Z0-9._-]/g, '_');
190
+ }
191
+ function sanitizeResult(result) {
192
+ return {
193
+ answerText: result.answerText,
194
+ answerMarkdown: result.answerMarkdown,
195
+ answerHtml: result.answerHtml,
196
+ tookMs: result.tookMs,
197
+ answerTokens: result.answerTokens,
198
+ answerChars: result.answerChars,
199
+ chromePid: undefined,
200
+ chromePort: undefined,
201
+ userDataDir: undefined,
202
+ };
203
+ }
204
+ function formatSocket(req) {
205
+ const socket = req.socket;
206
+ const host = socket.remoteAddress ?? 'unknown';
207
+ const port = socket.remotePort ?? '0';
208
+ return `${host}:${port}`;
209
+ }
210
+ function formatReachableAddresses(bindAddress, port) {
211
+ const ipv4 = [];
212
+ const ipv6 = [];
213
+ if (bindAddress && bindAddress !== '::' && bindAddress !== '0.0.0.0') {
214
+ if (bindAddress.includes(':')) {
215
+ ipv6.push(`[${bindAddress}]:${port}`);
216
+ }
217
+ else {
218
+ ipv4.push(`${bindAddress}:${port}`);
219
+ }
220
+ }
221
+ try {
222
+ const interfaces = os.networkInterfaces();
223
+ for (const entries of Object.values(interfaces)) {
224
+ if (!entries)
225
+ continue;
226
+ for (const entry of entries) {
227
+ const iface = entry;
228
+ if (!iface || iface.internal)
229
+ continue;
230
+ const family = typeof iface.family === 'string' ? iface.family : iface.family === 4 ? 'IPv4' : iface.family === 6 ? 'IPv6' : '';
231
+ if (family === 'IPv4') {
232
+ const addr = iface.address;
233
+ if (addr.startsWith('127.'))
234
+ continue;
235
+ if (addr.startsWith('169.254.'))
236
+ continue; // APIPA/link-local
237
+ ipv4.push(`${addr}:${port}`);
238
+ }
239
+ else if (family === 'IPv6') {
240
+ const addr = iface.address.toLowerCase();
241
+ if (addr === '::1' || addr.startsWith('fe80:'))
242
+ continue; // loopback/link-local
243
+ ipv6.push(`[${iface.address}]:${port}`);
244
+ }
245
+ }
246
+ }
247
+ }
248
+ catch {
249
+ // network interface probing can fail in locked-down environments; ignore
250
+ }
251
+ // de-dup
252
+ return Array.from(new Set([...ipv4, ...ipv6]));
253
+ }
254
+ async function loadLocalChatgptCookies(logger, targetUrl) {
255
+ try {
256
+ logger('Loading ChatGPT cookies from this host\'s Chrome profile...');
257
+ const cookies = await Promise.resolve(loadChromeCookies({
258
+ targetUrl,
259
+ profile: 'Default',
260
+ })).catch((error) => {
261
+ logger(`Unable to load local ChatGPT cookies on this host: ${error instanceof Error ? error.message : String(error)}`);
262
+ return [];
263
+ });
264
+ if (!cookies || cookies.length === 0) {
265
+ logger('No local ChatGPT cookies found on this host. Please log in once; opening ChatGPT...');
266
+ triggerLocalLoginPrompt(logger, targetUrl);
267
+ return null;
268
+ }
269
+ logger(`Loaded ${cookies.length} local ChatGPT cookies on this host.`);
270
+ return cookies;
271
+ }
272
+ catch (error) {
273
+ logger(`Unable to load local ChatGPT cookies on this host: ${error instanceof Error ? error.message : String(error)}`);
274
+ triggerLocalLoginPrompt(logger, targetUrl);
275
+ return null;
276
+ }
277
+ }
278
+ function triggerLocalLoginPrompt(logger, url) {
279
+ const opener = process.platform === 'darwin'
280
+ ? 'open'
281
+ : process.platform === 'win32'
282
+ ? 'start'
283
+ : 'xdg-open';
284
+ try {
285
+ // Fire and forget; user completes login in the opened browser window.
286
+ void import('node:child_process').then(({ spawn }) => {
287
+ spawn(opener, [url], { stdio: 'ignore', detached: true }).unref();
288
+ });
289
+ logger(`Opened ${url} locally. Please sign in; subsequent runs will reuse the session.`);
290
+ }
291
+ catch {
292
+ logger(`Please open ${url} in this host's browser and sign in; then rerun.`);
293
+ }
294
+ }
@@ -0,0 +1 @@
1
+ export {};