@thegitai/cli 1.0.0-preview.5 → 1.0.0-preview.51

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 (76) hide show
  1. package/README.md +93 -4
  2. package/dist/bin/ai.js +142 -112
  3. package/dist/bin/browser-host.js +265 -0
  4. package/dist/src/agent-mode.js +32 -7
  5. package/dist/src/api/auth.js +10 -4
  6. package/dist/src/api/browser-login.js +149 -37
  7. package/dist/src/api/chat.js +680 -166
  8. package/dist/src/api/contracts.js +55 -1
  9. package/dist/src/api/default-host.js +1 -0
  10. package/dist/src/api/http.js +151 -7
  11. package/dist/src/api/models.js +29 -10
  12. package/dist/src/api/startup-handshake.js +36 -0
  13. package/dist/src/api/turn-recovery.js +189 -0
  14. package/dist/src/background-jobs.js +4 -2
  15. package/dist/src/browser/bridge.js +262 -0
  16. package/dist/src/browser/framing.js +42 -0
  17. package/dist/src/browser/native-host.js +219 -0
  18. package/dist/src/browser/protocol.js +46 -0
  19. package/dist/src/browser/session-bridge.js +130 -0
  20. package/dist/src/cli-args.js +19 -5
  21. package/dist/src/client-environment.js +2 -0
  22. package/dist/src/core/clipboard.js +7 -13
  23. package/dist/src/core/image-limits.js +56 -0
  24. package/dist/src/core/image-path-extractor.js +70 -3
  25. package/dist/src/core/session-image-store.js +246 -0
  26. package/dist/src/edit-journal.js +33 -2
  27. package/dist/src/executor.js +25 -3
  28. package/dist/src/help-text.js +112 -21
  29. package/dist/src/patcher.js +43 -4
  30. package/dist/src/permissions.js +288 -0
  31. package/dist/src/preferences.js +39 -0
  32. package/dist/src/quota-notice.js +35 -0
  33. package/dist/src/session-safety.js +0 -12
  34. package/dist/src/session-store.js +121 -20
  35. package/dist/src/session.js +19 -4
  36. package/dist/src/signin.js +58 -0
  37. package/dist/src/tool-executor.js +13 -46
  38. package/dist/src/tools/browser.js +641 -0
  39. package/dist/src/tools/delete-file.js +20 -4
  40. package/dist/src/tools/index.js +32 -10
  41. package/dist/src/tools/patch-file.js +12 -26
  42. package/dist/src/tools/read-file.js +5 -0
  43. package/dist/src/tools/read-image-file.js +85 -0
  44. package/dist/src/tools/replace-document-text.js +28 -18
  45. package/dist/src/tools/restore-checkpoint.js +0 -1
  46. package/dist/src/tools/run-command.js +14 -71
  47. package/dist/src/tools/run-node-script.js +12 -81
  48. package/dist/src/tools/save-generated-image.js +120 -0
  49. package/dist/src/tools/str-replace.js +12 -26
  50. package/dist/src/tools/undo-edit.js +1 -6
  51. package/dist/src/tools/write-file.js +67 -11
  52. package/dist/src/ui/prompt-history-store.js +1 -1
  53. package/dist/src/ui/repl.js +949 -279
  54. package/dist/src/ui/slash-commands.js +121 -0
  55. package/dist/src/ui/tui/attention.js +25 -0
  56. package/dist/src/ui/tui/bridge.js +38 -5
  57. package/dist/src/ui/tui/build-frame.js +686 -223
  58. package/dist/src/ui/tui/composer-layout.js +76 -0
  59. package/dist/src/ui/tui/desktop-notification.js +118 -0
  60. package/dist/src/ui/tui/markdown-render.js +11 -2
  61. package/dist/src/ui/tui/shell-input.js +256 -73
  62. package/dist/src/ui/tui/shortcuts.js +61 -0
  63. package/dist/src/ui/tui/terminal-theme.js +28 -0
  64. package/dist/src/ui/tui/user-input.js +593 -0
  65. package/dist/src/update-check.js +205 -0
  66. package/dist/src/usage.js +5 -2
  67. package/dist/src/utils.js +13 -0
  68. package/dist/src/version.js +52 -2
  69. package/package.json +22 -9
  70. package/dist/src/project-index.js +0 -233
  71. package/dist/src/tools/code-intel.js +0 -472
  72. package/dist/src/tools/find-symbol.js +0 -70
  73. package/dist/src/tools/hover-symbol.js +0 -95
  74. package/dist/src/tools/list-symbols.js +0 -55
  75. package/dist/src/tools/search-code.js +0 -37
  76. package/dist/src/tools/signature-help.js +0 -118
@@ -0,0 +1,262 @@
1
+ import crypto from 'node:crypto';
2
+ import fs from 'node:fs';
3
+ import net from 'node:net';
4
+ import path from 'node:path';
5
+ import { registerNativeHost } from './native-host.js';
6
+ import { BROWSER_PROTOCOL_VERSION, browserRuntimeDir, protocolMismatchMessage, sessionKeyFromRecordFile, sessionRecordPath, sessionSocketPath, socketIsFile, } from './protocol.js';
7
+ const DEFAULT_TIMEOUT_MS = 30000;
8
+ export class BrowserBridgeError extends Error {
9
+ code;
10
+ constructor(message, code = 'browser_error') {
11
+ super(message);
12
+ this.name = 'BrowserBridgeError';
13
+ this.code = code;
14
+ }
15
+ }
16
+ function rejectDisconnectedRequests(browser) {
17
+ for (const pending of browser.pending.values()) {
18
+ clearTimeout(pending.timer);
19
+ pending.reject(new BrowserBridgeError(`The browser disconnected while ${pending.op} was in flight, so its outcome is unknown. Check the page before doing anything that could repeat.`, 'disconnected'));
20
+ }
21
+ browser.pending.clear();
22
+ }
23
+ function safeUnlink(target) {
24
+ try {
25
+ fs.unlinkSync(target);
26
+ }
27
+ catch {
28
+ }
29
+ }
30
+ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env, }) {
31
+ const dir = browserRuntimeDir(env);
32
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
33
+ const socketPath = sessionSocketPath(dir, sessionKey);
34
+ const recordPath = sessionRecordPath(dir, sessionKey);
35
+ if (socketIsFile())
36
+ safeUnlink(socketPath);
37
+ const browsers = new Map();
38
+ const sockets = new Set();
39
+ const listeners = new Set();
40
+ let selected = null;
41
+ let everConnected = false;
42
+ let closed = false;
43
+ const registration = registerNativeHost({ env });
44
+ const server = net.createServer((socket) => {
45
+ socket.setNoDelay(true);
46
+ sockets.add(socket);
47
+ let buffer = '';
48
+ let key = null;
49
+ socket.on('data', (chunk) => {
50
+ if (socket.destroyed)
51
+ return;
52
+ buffer += chunk.toString('utf8');
53
+ let newline = buffer.indexOf('\n');
54
+ while (newline !== -1) {
55
+ const line = buffer.slice(0, newline);
56
+ buffer = buffer.slice(newline + 1);
57
+ newline = buffer.indexOf('\n');
58
+ if (!line.trim())
59
+ continue;
60
+ let frame;
61
+ try {
62
+ frame = JSON.parse(line);
63
+ }
64
+ catch {
65
+ continue;
66
+ }
67
+ if (frame.t === 'hello') {
68
+ if (frame.protocol !== BROWSER_PROTOCOL_VERSION) {
69
+ const message = protocolMismatchMessage(frame.protocol);
70
+ for (const listener of listeners)
71
+ listener('protocol_mismatch', { message });
72
+ socket.destroy();
73
+ return;
74
+ }
75
+ const announced = frame.browser?.key ?? crypto.randomUUID();
76
+ const existing = key ? browsers.get(key) : null;
77
+ const displaced = browsers.get(announced);
78
+ if (displaced && displaced.socket !== socket) {
79
+ displaced.socket.destroy();
80
+ rejectDisconnectedRequests(displaced);
81
+ }
82
+ if (existing?.socket === socket) {
83
+ if (announced !== key) {
84
+ browsers.delete(key);
85
+ browsers.set(announced, existing);
86
+ if (selected === key)
87
+ selected = announced;
88
+ }
89
+ existing.identity = frame.browser;
90
+ existing.socket = socket;
91
+ }
92
+ else {
93
+ browsers.set(announced, {
94
+ identity: frame.browser,
95
+ socket,
96
+ pending: new Map(),
97
+ });
98
+ if (!selected)
99
+ selected = announced;
100
+ }
101
+ key = announced;
102
+ everConnected = true;
103
+ for (const listener of listeners) {
104
+ listener('connected', { browser: frame.browser });
105
+ }
106
+ continue;
107
+ }
108
+ const browser = key ? browsers.get(key) : null;
109
+ if (!browser || browser.socket !== socket)
110
+ continue;
111
+ if (frame.t === 'res') {
112
+ const pending = browser.pending.get(frame.id);
113
+ if (!pending)
114
+ continue;
115
+ browser.pending.delete(frame.id);
116
+ clearTimeout(pending.timer);
117
+ if (frame.ok)
118
+ pending.resolve(frame.result);
119
+ else
120
+ pending.reject(new BrowserBridgeError(frame.error ?? 'Browser operation failed.', frame.code ?? 'browser_error'));
121
+ continue;
122
+ }
123
+ if (frame.t === 'event') {
124
+ for (const listener of listeners)
125
+ listener(frame.event, frame.data ?? {});
126
+ }
127
+ }
128
+ });
129
+ const drop = () => {
130
+ sockets.delete(socket);
131
+ if (!key)
132
+ return;
133
+ const browser = browsers.get(key);
134
+ if (!browser || browser.socket !== socket)
135
+ return;
136
+ browsers.delete(key);
137
+ if (selected === key)
138
+ selected = browsers.keys().next().value ?? null;
139
+ rejectDisconnectedRequests(browser);
140
+ for (const listener of listeners)
141
+ listener('disconnected', { browser: key });
142
+ };
143
+ socket.on('close', drop);
144
+ socket.on('error', drop);
145
+ });
146
+ server.listen(socketPath, () => {
147
+ try {
148
+ fs.chmodSync(socketPath, 0o600);
149
+ }
150
+ catch {
151
+ }
152
+ const announcement = {
153
+ protocol: BROWSER_PROTOCOL_VERSION,
154
+ session: sessionKey,
155
+ pid: process.pid,
156
+ label,
157
+ cwd,
158
+ startedAt: new Date().toISOString(),
159
+ };
160
+ try {
161
+ fs.writeFileSync(recordPath, `${JSON.stringify(announcement)}\n`, { mode: 0o600 });
162
+ }
163
+ catch {
164
+ }
165
+ });
166
+ server.on('error', () => {
167
+ });
168
+ const close = () => {
169
+ if (closed)
170
+ return;
171
+ closed = true;
172
+ for (const browser of browsers.values())
173
+ rejectDisconnectedRequests(browser);
174
+ for (const socket of sockets)
175
+ socket.destroy();
176
+ sockets.clear();
177
+ browsers.clear();
178
+ server.close();
179
+ safeUnlink(socketPath);
180
+ safeUnlink(recordPath);
181
+ };
182
+ process.once('exit', close);
183
+ return {
184
+ sessionKey,
185
+ status() {
186
+ return {
187
+ connected: browsers.size > 0,
188
+ everConnected,
189
+ browsers: [...browsers.values()].map((browser) => browser.identity),
190
+ selected,
191
+ socketPath,
192
+ registrationErrors: registration.errors,
193
+ };
194
+ },
195
+ resync() {
196
+ if (closed || browsers.size > 0 || sockets.size === 0)
197
+ return false;
198
+ for (const socket of sockets)
199
+ socket.destroy();
200
+ sockets.clear();
201
+ return true;
202
+ },
203
+ selectBrowser(key) {
204
+ if (browsers.has(key))
205
+ selected = key;
206
+ },
207
+ onEvent(listener) {
208
+ listeners.add(listener);
209
+ return () => listeners.delete(listener);
210
+ },
211
+ request(op, args = {}, options = {}) {
212
+ const target = selected ? browsers.get(selected) : null;
213
+ if (!target) {
214
+ return Promise.reject(new BrowserBridgeError('No browser is connected to this session.', 'not_connected'));
215
+ }
216
+ const id = crypto.randomUUID();
217
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
218
+ return new Promise((resolve, reject) => {
219
+ const timer = setTimeout(() => {
220
+ target.pending.delete(id);
221
+ reject(new BrowserBridgeError(`The browser did not answer ${op} within ${Math.round(timeoutMs / 1000)}s, so its outcome is unknown. Look at the page before repeating anything that changes state.`, 'timeout'));
222
+ }, timeoutMs);
223
+ target.pending.set(id, { resolve, reject, timer, op });
224
+ target.socket.write(`${JSON.stringify({ t: 'req', id, op, args })}\n`, (error) => {
225
+ if (!error)
226
+ return;
227
+ target.pending.delete(id);
228
+ clearTimeout(timer);
229
+ reject(new BrowserBridgeError(`Could not reach the browser: ${error.message}`, 'write_failed'));
230
+ });
231
+ });
232
+ },
233
+ close,
234
+ };
235
+ }
236
+ export function pruneStaleSessions(env = process.env) {
237
+ const dir = browserRuntimeDir(env);
238
+ let entries;
239
+ try {
240
+ entries = fs.readdirSync(dir);
241
+ }
242
+ catch {
243
+ return;
244
+ }
245
+ for (const entry of entries) {
246
+ const key = sessionKeyFromRecordFile(entry);
247
+ if (!key)
248
+ continue;
249
+ const file = path.join(dir, entry);
250
+ try {
251
+ const record = JSON.parse(fs.readFileSync(file, 'utf8'));
252
+ process.kill(record.pid, 0);
253
+ }
254
+ catch (error) {
255
+ if (error?.code === 'EPERM')
256
+ continue;
257
+ safeUnlink(file);
258
+ if (socketIsFile())
259
+ safeUnlink(sessionSocketPath(dir, key));
260
+ }
261
+ }
262
+ }
@@ -0,0 +1,42 @@
1
+ export const MAX_FRAME_BYTES = 900 * 1024;
2
+ export const FRAGMENT_BYTES = 512 * 1024;
3
+ export function encodeFrame(payload) {
4
+ const header = Buffer.alloc(4);
5
+ header.writeUInt32LE(payload.length, 0);
6
+ return Buffer.concat([header, payload]);
7
+ }
8
+ export function fragmentPayload(payload, id) {
9
+ const total = Math.ceil(payload.length / FRAGMENT_BYTES);
10
+ const frames = [];
11
+ for (let part = 0; part < total; part += 1) {
12
+ frames.push({
13
+ t: 'fragment',
14
+ id,
15
+ part,
16
+ total,
17
+ data: payload.subarray(part * FRAGMENT_BYTES, (part + 1) * FRAGMENT_BYTES).toString('base64'),
18
+ });
19
+ }
20
+ return frames;
21
+ }
22
+ export function decodeFrames(inbound) {
23
+ const frames = [];
24
+ let rest = inbound;
25
+ let unparseable = 0;
26
+ for (;;) {
27
+ if (rest.length < 4)
28
+ break;
29
+ const length = rest.readUInt32LE(0);
30
+ if (rest.length < 4 + length)
31
+ break;
32
+ const body = rest.subarray(4, 4 + length);
33
+ rest = rest.subarray(4 + length);
34
+ try {
35
+ frames.push(JSON.parse(body.toString('utf8')));
36
+ }
37
+ catch {
38
+ unparseable += 1;
39
+ }
40
+ }
41
+ return { frames, rest, unparseable };
42
+ }
@@ -0,0 +1,219 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { EXTENSION_IDS, NATIVE_HOST_NAME } from './protocol.js';
7
+ function browserConfigRoots(env, platform) {
8
+ const home = env.HOME ?? os.homedir();
9
+ if (platform === 'win32') {
10
+ const local = env.LOCALAPPDATA ?? path.join(home, 'AppData', 'Local');
11
+ return [
12
+ path.join(local, 'Google', 'Chrome', 'User Data'),
13
+ path.join(local, 'Google', 'Chrome Beta', 'User Data'),
14
+ path.join(local, 'Chromium', 'User Data'),
15
+ path.join(local, 'Microsoft', 'Edge', 'User Data'),
16
+ path.join(local, 'BraveSoftware', 'Brave-Browser', 'User Data'),
17
+ path.join(local, 'Vivaldi', 'User Data'),
18
+ ];
19
+ }
20
+ if (platform === 'darwin') {
21
+ const support = path.join(home, 'Library', 'Application Support');
22
+ return [
23
+ path.join(support, 'Google', 'Chrome'),
24
+ path.join(support, 'Google', 'Chrome Beta'),
25
+ path.join(support, 'Chromium'),
26
+ path.join(support, 'Microsoft Edge'),
27
+ path.join(support, 'BraveSoftware', 'Brave-Browser'),
28
+ path.join(support, 'Vivaldi'),
29
+ path.join(support, 'Arc', 'User Data'),
30
+ ];
31
+ }
32
+ const configHome = env.XDG_CONFIG_HOME ?? path.join(home, '.config');
33
+ return [
34
+ path.join(configHome, 'google-chrome'),
35
+ path.join(configHome, 'google-chrome-beta'),
36
+ path.join(configHome, 'chromium'),
37
+ path.join(configHome, 'microsoft-edge'),
38
+ path.join(configHome, 'BraveSoftware', 'Brave-Browser'),
39
+ path.join(configHome, 'vivaldi'),
40
+ ];
41
+ }
42
+ function thegitaiHome(env, platform = process.platform) {
43
+ if (platform === 'win32') {
44
+ const local = env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
45
+ return env.THEGITAI_HOME ?? path.join(local, 'thegitai');
46
+ }
47
+ return env.THEGITAI_HOME ?? path.join(env.HOME ?? os.homedir(), '.thegitai');
48
+ }
49
+ function writeLauncher(env, hostScript, platform = process.platform) {
50
+ const dir = thegitaiHome(env, platform);
51
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
52
+ if (platform === 'win32') {
53
+ const launcherPath = path.join(dir, 'browser-bridge-host.bat');
54
+ const contents = [
55
+ '@echo off',
56
+ `"${process.execPath}" "${hostScript}" %*`,
57
+ '',
58
+ ].join('\r\n');
59
+ const existingBat = fs.existsSync(launcherPath)
60
+ ? fs.readFileSync(launcherPath, 'utf8')
61
+ : null;
62
+ if (existingBat === contents)
63
+ return { launcherPath, changed: false };
64
+ fs.writeFileSync(launcherPath, contents);
65
+ return { launcherPath, changed: true };
66
+ }
67
+ const launcherPath = path.join(dir, 'browser-bridge-host.sh');
68
+ const quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
69
+ const contents = `#!/bin/sh\nexec ${quote(process.execPath)} ${quote(hostScript)} "$@"\n`;
70
+ const existing = fs.existsSync(launcherPath) ? fs.readFileSync(launcherPath, 'utf8') : null;
71
+ if (existing === contents) {
72
+ try {
73
+ fs.chmodSync(launcherPath, 0o700);
74
+ }
75
+ catch {
76
+ }
77
+ return { launcherPath, changed: false };
78
+ }
79
+ fs.writeFileSync(launcherPath, contents, { mode: 0o700 });
80
+ fs.chmodSync(launcherPath, 0o700);
81
+ return { launcherPath, changed: true };
82
+ }
83
+ function resolveHostScript() {
84
+ const here = path.dirname(fileURLToPath(import.meta.url));
85
+ return path.resolve(here, '..', '..', 'bin', 'browser-host.js');
86
+ }
87
+ export function registerNativeHost({ env = process.env, platform = process.platform, hostScript = resolveHostScript(), } = {}) {
88
+ const errors = [];
89
+ const manifests = [];
90
+ let changed = false;
91
+ const { launcherPath, changed: launcherChanged } = writeLauncher(env, hostScript, platform);
92
+ changed = launcherChanged;
93
+ const manifest = {
94
+ name: NATIVE_HOST_NAME,
95
+ description: 'TheGitAI browser bridge',
96
+ path: launcherPath,
97
+ type: 'stdio',
98
+ allowed_origins: EXTENSION_IDS.map((id) => `chrome-extension://${id}/`),
99
+ };
100
+ const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
101
+ if (platform === 'win32') {
102
+ return registerWindowsNativeHost({ env, serialized, launcherPath, changed });
103
+ }
104
+ for (const root of browserConfigRoots(env, platform)) {
105
+ if (!fs.existsSync(root))
106
+ continue;
107
+ const dir = path.join(root, 'NativeMessagingHosts');
108
+ const file = path.join(dir, `${NATIVE_HOST_NAME}.json`);
109
+ try {
110
+ fs.mkdirSync(dir, { recursive: true });
111
+ const existing = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null;
112
+ if (existing !== serialized) {
113
+ fs.writeFileSync(file, serialized, { mode: 0o600 });
114
+ changed = true;
115
+ }
116
+ manifests.push(file);
117
+ }
118
+ catch (error) {
119
+ errors.push(`${file}: ${error?.message ?? error}`);
120
+ }
121
+ }
122
+ return { manifests, launcherPath, changed, errors };
123
+ }
124
+ const WINDOWS_REGISTRY_ROOTS = [
125
+ { userData: 'Google\\Chrome', key: 'Software\\Google\\Chrome' },
126
+ { userData: 'Google\\Chrome Beta', key: 'Software\\Google\\Chrome Beta' },
127
+ { userData: 'Chromium', key: 'Software\\Chromium' },
128
+ { userData: 'Microsoft\\Edge', key: 'Software\\Microsoft\\Edge' },
129
+ { userData: 'BraveSoftware\\Brave-Browser', key: 'Software\\BraveSoftware\\Brave-Browser' },
130
+ { userData: 'Vivaldi', key: 'Software\\Vivaldi' },
131
+ ];
132
+ function registerWindowsNativeHost({ env, serialized, launcherPath, changed, }) {
133
+ const errors = [];
134
+ const manifests = [];
135
+ let dirty = changed;
136
+ const manifestPath = path.join(thegitaiHome(env, 'win32'), `${NATIVE_HOST_NAME}.json`);
137
+ try {
138
+ fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
139
+ const existing = fs.existsSync(manifestPath)
140
+ ? fs.readFileSync(manifestPath, 'utf8')
141
+ : null;
142
+ if (existing !== serialized) {
143
+ fs.writeFileSync(manifestPath, serialized);
144
+ dirty = true;
145
+ }
146
+ manifests.push(manifestPath);
147
+ }
148
+ catch (error) {
149
+ errors.push(`${manifestPath}: ${error?.message ?? error}`);
150
+ return { manifests, launcherPath, changed: dirty, errors };
151
+ }
152
+ const local = env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
153
+ for (const browser of WINDOWS_REGISTRY_ROOTS) {
154
+ if (!fs.existsSync(path.join(local, browser.userData, 'User Data')))
155
+ continue;
156
+ const key = `HKCU\\${browser.key}\\NativeMessagingHosts\\${NATIVE_HOST_NAME}`;
157
+ try {
158
+ const current = readRegistryDefault(key);
159
+ if (current === manifestPath)
160
+ continue;
161
+ execFileSync('reg.exe', ['add', key, '/ve', '/t', 'REG_SZ', '/d', manifestPath, '/f'], { timeout: 5000, stdio: ['ignore', 'ignore', 'pipe'] });
162
+ dirty = true;
163
+ }
164
+ catch (error) {
165
+ errors.push(`${key}: ${error?.message ?? error}`);
166
+ }
167
+ }
168
+ return { manifests, launcherPath, changed: dirty, errors };
169
+ }
170
+ function readRegistryDefault(key) {
171
+ try {
172
+ const output = execFileSync('reg.exe', ['query', key, '/ve'], {
173
+ encoding: 'utf8',
174
+ timeout: 5000,
175
+ stdio: ['ignore', 'pipe', 'ignore'],
176
+ });
177
+ const match = /\(Default\)\s+REG_SZ\s+(.+?)\s*$/m.exec(output);
178
+ return match ? match[1] : null;
179
+ }
180
+ catch {
181
+ return null;
182
+ }
183
+ }
184
+ export function chromiumProcessRunning(platform = process.platform) {
185
+ if (platform === 'win32') {
186
+ try {
187
+ const output = execFileSync('tasklist.exe', ['/fo', 'csv', '/nh'], {
188
+ encoding: 'utf8',
189
+ timeout: 4000,
190
+ stdio: ['ignore', 'pipe', 'ignore'],
191
+ });
192
+ return /"(chrome|chromium|msedge|brave|vivaldi)\.exe"/i.test(output);
193
+ }
194
+ catch {
195
+ return false;
196
+ }
197
+ }
198
+ try {
199
+ const output = execFileSync('ps', ['-eo', 'comm='], {
200
+ encoding: 'utf8',
201
+ timeout: 2000,
202
+ stdio: ['ignore', 'pipe', 'ignore'],
203
+ });
204
+ return output.split('\n').some((line) => isBrowserProcessName(line));
205
+ }
206
+ catch {
207
+ return false;
208
+ }
209
+ }
210
+ const BROWSER_PROCESS_NAME = /^(google[ -]?chrome|chrome|chromium|brave|msedge|microsoft[ -]?edge|vivaldi)/i;
211
+ const NOT_A_BROWSER_PROCESS = /crashpad|crashhandler|native|sandbox|driver|updater?\b/i;
212
+ export function isBrowserProcessName(psLine) {
213
+ const name = psLine.trim().split('/').pop()?.trim() ?? '';
214
+ if (!name)
215
+ return false;
216
+ if (NOT_A_BROWSER_PROCESS.test(name))
217
+ return false;
218
+ return BROWSER_PROCESS_NAME.test(name);
219
+ }
@@ -0,0 +1,46 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ export const BROWSER_PROTOCOL_VERSION = 1;
4
+ export const NATIVE_HOST_NAME = 'ai.thegit.browser_bridge';
5
+ export const EXTENSION_IDS = [
6
+ 'neleeiihdnddldafklledbkembkfagol',
7
+ 'jipbjphlelfilkhdjkfdficafjhgjfmk',
8
+ ];
9
+ export function browserRuntimeDir(env = process.env, platform = process.platform) {
10
+ if (platform === 'win32') {
11
+ const local = env.LOCALAPPDATA ?? path.join(os.homedir(), 'AppData', 'Local');
12
+ return path.join(env.THEGITAI_HOME ?? path.join(local, 'thegitai'), 'browser');
13
+ }
14
+ const runtime = env.XDG_RUNTIME_DIR;
15
+ if (runtime && path.isAbsolute(runtime)) {
16
+ return path.join(runtime, 'thegitai', 'browser');
17
+ }
18
+ return path.join(env.THEGITAI_HOME ?? path.join(os.homedir(), '.thegitai'), 'browser');
19
+ }
20
+ const WINDOWS_PIPE_PREFIX = '\\\\.\\pipe\\';
21
+ export function sessionSocketPath(dir, sessionKey, platform = process.platform) {
22
+ if (platform === 'win32') {
23
+ return `${WINDOWS_PIPE_PREFIX}thegitai-browser-${sessionKey}`;
24
+ }
25
+ return path.join(dir, `s-${sessionKey}.sock`);
26
+ }
27
+ export function socketIsFile(platform = process.platform) {
28
+ return platform !== 'win32';
29
+ }
30
+ export function sessionKeyFromRecordFile(fileName) {
31
+ const match = /^s-(.+)\.json$/.exec(fileName);
32
+ return match ? match[1] : null;
33
+ }
34
+ export function sessionRecordPath(dir, sessionKey) {
35
+ return path.join(dir, `s-${sessionKey}.json`);
36
+ }
37
+ export function protocolMismatchMessage(theirs) {
38
+ const version = Number(theirs);
39
+ if (!Number.isInteger(version)) {
40
+ return 'The TheGitAI extension did not report a protocol version. Update both TheGitAI and the extension.';
41
+ }
42
+ if (version < BROWSER_PROTOCOL_VERSION) {
43
+ return `The TheGitAI extension speaks browser protocol v${version}; this CLI speaks v${BROWSER_PROTOCOL_VERSION}. Update the extension from the Chrome Web Store.`;
44
+ }
45
+ return `The TheGitAI extension speaks browser protocol v${version}; this CLI speaks v${BROWSER_PROTOCOL_VERSION}. Update TheGitAI: npm i -g @thegitai/cli`;
46
+ }