@thegitai/cli 1.0.0-preview.46 → 1.0.0-preview.48
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.
|
@@ -13,6 +13,13 @@ export class BrowserBridgeError extends Error {
|
|
|
13
13
|
this.code = code;
|
|
14
14
|
}
|
|
15
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
|
+
}
|
|
16
23
|
function safeUnlink(target) {
|
|
17
24
|
try {
|
|
18
25
|
fs.unlinkSync(target);
|
|
@@ -28,15 +35,20 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
28
35
|
if (socketIsFile())
|
|
29
36
|
safeUnlink(socketPath);
|
|
30
37
|
const browsers = new Map();
|
|
38
|
+
const sockets = new Set();
|
|
31
39
|
const listeners = new Set();
|
|
32
40
|
let selected = null;
|
|
41
|
+
let everConnected = false;
|
|
33
42
|
let closed = false;
|
|
34
43
|
const registration = registerNativeHost({ env });
|
|
35
44
|
const server = net.createServer((socket) => {
|
|
36
45
|
socket.setNoDelay(true);
|
|
46
|
+
sockets.add(socket);
|
|
37
47
|
let buffer = '';
|
|
38
48
|
let key = null;
|
|
39
49
|
socket.on('data', (chunk) => {
|
|
50
|
+
if (socket.destroyed)
|
|
51
|
+
return;
|
|
40
52
|
buffer += chunk.toString('utf8');
|
|
41
53
|
let newline = buffer.indexOf('\n');
|
|
42
54
|
while (newline !== -1) {
|
|
@@ -62,7 +74,12 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
62
74
|
}
|
|
63
75
|
const announced = frame.browser?.key ?? crypto.randomUUID();
|
|
64
76
|
const existing = key ? browsers.get(key) : null;
|
|
65
|
-
|
|
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) {
|
|
66
83
|
if (announced !== key) {
|
|
67
84
|
browsers.delete(key);
|
|
68
85
|
browsers.set(announced, existing);
|
|
@@ -82,13 +99,14 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
82
99
|
selected = announced;
|
|
83
100
|
}
|
|
84
101
|
key = announced;
|
|
102
|
+
everConnected = true;
|
|
85
103
|
for (const listener of listeners) {
|
|
86
104
|
listener('connected', { browser: frame.browser });
|
|
87
105
|
}
|
|
88
106
|
continue;
|
|
89
107
|
}
|
|
90
108
|
const browser = key ? browsers.get(key) : null;
|
|
91
|
-
if (!browser)
|
|
109
|
+
if (!browser || browser.socket !== socket)
|
|
92
110
|
continue;
|
|
93
111
|
if (frame.t === 'res') {
|
|
94
112
|
const pending = browser.pending.get(frame.id);
|
|
@@ -109,16 +127,16 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
109
127
|
}
|
|
110
128
|
});
|
|
111
129
|
const drop = () => {
|
|
130
|
+
sockets.delete(socket);
|
|
112
131
|
if (!key)
|
|
113
132
|
return;
|
|
114
133
|
const browser = browsers.get(key);
|
|
134
|
+
if (!browser || browser.socket !== socket)
|
|
135
|
+
return;
|
|
115
136
|
browsers.delete(key);
|
|
116
137
|
if (selected === key)
|
|
117
138
|
selected = browsers.keys().next().value ?? null;
|
|
118
|
-
|
|
119
|
-
clearTimeout(pending.timer);
|
|
120
|
-
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'));
|
|
121
|
-
}
|
|
139
|
+
rejectDisconnectedRequests(browser);
|
|
122
140
|
for (const listener of listeners)
|
|
123
141
|
listener('disconnected', { browser: key });
|
|
124
142
|
};
|
|
@@ -152,7 +170,10 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
152
170
|
return;
|
|
153
171
|
closed = true;
|
|
154
172
|
for (const browser of browsers.values())
|
|
155
|
-
browser
|
|
173
|
+
rejectDisconnectedRequests(browser);
|
|
174
|
+
for (const socket of sockets)
|
|
175
|
+
socket.destroy();
|
|
176
|
+
sockets.clear();
|
|
156
177
|
browsers.clear();
|
|
157
178
|
server.close();
|
|
158
179
|
safeUnlink(socketPath);
|
|
@@ -164,12 +185,21 @@ export function startBrowserBridge({ sessionKey, label, cwd, env = process.env,
|
|
|
164
185
|
status() {
|
|
165
186
|
return {
|
|
166
187
|
connected: browsers.size > 0,
|
|
188
|
+
everConnected,
|
|
167
189
|
browsers: [...browsers.values()].map((browser) => browser.identity),
|
|
168
190
|
selected,
|
|
169
191
|
socketPath,
|
|
170
192
|
registrationErrors: registration.errors,
|
|
171
193
|
};
|
|
172
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
|
+
},
|
|
173
203
|
selectBrowser(key) {
|
|
174
204
|
if (browsers.has(key))
|
|
175
205
|
selected = key;
|
|
@@ -201,9 +201,19 @@ export function chromiumProcessRunning(platform = process.platform) {
|
|
|
201
201
|
timeout: 2000,
|
|
202
202
|
stdio: ['ignore', 'pipe', 'ignore'],
|
|
203
203
|
});
|
|
204
|
-
return
|
|
204
|
+
return output.split('\n').some((line) => isBrowserProcessName(line));
|
|
205
205
|
}
|
|
206
206
|
catch {
|
|
207
207
|
return false;
|
|
208
208
|
}
|
|
209
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
|
+
}
|
|
@@ -86,19 +86,45 @@ export function browserAvailability(options) {
|
|
|
86
86
|
if (status.connected) {
|
|
87
87
|
return {
|
|
88
88
|
connected: true,
|
|
89
|
+
everConnected: true,
|
|
89
90
|
browsers: status.browsers,
|
|
90
91
|
selected: status.selected,
|
|
91
92
|
};
|
|
92
93
|
}
|
|
93
|
-
const hint =
|
|
94
|
-
|
|
95
|
-
:
|
|
96
|
-
|
|
97
|
-
|
|
94
|
+
const hint = connectionHint({
|
|
95
|
+
registrationError: status.registrationErrors[0] ?? null,
|
|
96
|
+
everConnected: status.everConnected,
|
|
97
|
+
chromeRunning: () => chromiumProcessRunning(),
|
|
98
|
+
extensionSeen: () => browserExtensionSeen(options.env),
|
|
99
|
+
});
|
|
98
100
|
return {
|
|
99
101
|
connected: false,
|
|
102
|
+
everConnected: status.everConnected,
|
|
100
103
|
browsers: [],
|
|
101
104
|
selected: null,
|
|
102
105
|
hint,
|
|
103
106
|
};
|
|
104
107
|
}
|
|
108
|
+
export function connectionHint(state) {
|
|
109
|
+
if (state.registrationError) {
|
|
110
|
+
return `TheGitAI could not register its browser bridge with Chrome: ${state.registrationError}`;
|
|
111
|
+
}
|
|
112
|
+
if (state.everConnected) {
|
|
113
|
+
return 'The browser was connected to this session but is disconnected now. Check Chrome and the connection status shown by the TheGitAI extension.';
|
|
114
|
+
}
|
|
115
|
+
if (!state.chromeRunning()) {
|
|
116
|
+
return 'Chrome does not appear to be running. Open Chrome with the TheGitAI extension enabled and check its connection status.';
|
|
117
|
+
}
|
|
118
|
+
if (state.extensionSeen()) {
|
|
119
|
+
return 'The TheGitAI extension has connected on this machine before, but has not connected to this session yet. Check the connection status shown by the extension.';
|
|
120
|
+
}
|
|
121
|
+
return 'Chrome is running, but no browser connection reached this session. Check that the TheGitAI extension is installed and enabled, and inspect its connection status.';
|
|
122
|
+
}
|
|
123
|
+
export function resyncBrowserBridge(options) {
|
|
124
|
+
try {
|
|
125
|
+
return ensureBrowserBridge(options).resync();
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -3,11 +3,12 @@ import path from 'node:path';
|
|
|
3
3
|
import { normalizeProjectRelativePath } from '../artifact-policy.js';
|
|
4
4
|
import { BrowserBridgeError } from '../browser/bridge.js';
|
|
5
5
|
import { storeSessionImageBytes } from '../core/session-image-store.js';
|
|
6
|
-
import { browserAvailability, drainBrowserEvents, ensureBrowserBridge, } from '../browser/session-bridge.js';
|
|
6
|
+
import { browserAvailability, drainBrowserEvents, ensureBrowserBridge, resyncBrowserBridge, } from '../browser/session-bridge.js';
|
|
7
7
|
import { ensurePermission, originForUrl } from '../permissions.js';
|
|
8
8
|
const READ_TIMEOUT_MS = 20000;
|
|
9
9
|
const NAVIGATE_TIMEOUT_MS = 45000;
|
|
10
10
|
const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
11
|
+
const DISCOVERY_TIMEOUT_MS = 2500;
|
|
11
12
|
export const READ_ONLY_BROWSER_TOOLS = new Set([
|
|
12
13
|
'browser',
|
|
13
14
|
'browser_tabs',
|
|
@@ -31,25 +32,33 @@ function missing(argument, tool) {
|
|
|
31
32
|
},
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
async function waitForBrowser(options, timeoutMs) {
|
|
36
|
+
const deadline = Date.now() + timeoutMs;
|
|
37
|
+
const bridge = ensureBrowserBridge(options);
|
|
38
|
+
while (!bridge.status().connected && Date.now() < deadline) {
|
|
39
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
40
|
+
}
|
|
41
|
+
return browserAvailability(options);
|
|
42
|
+
}
|
|
43
|
+
async function notConnected(context) {
|
|
44
|
+
const options = { cwd: context.rootDir, env: context.env };
|
|
45
|
+
const status = ensureBrowserBridge(options).status();
|
|
46
|
+
if (status.connected)
|
|
47
|
+
return { ok: true };
|
|
48
|
+
if (status.everConnected)
|
|
49
|
+
resyncBrowserBridge(options);
|
|
50
|
+
const availability = await waitForBrowser(options, DISCOVERY_TIMEOUT_MS);
|
|
39
51
|
if (availability.connected)
|
|
40
52
|
return { ok: true };
|
|
41
53
|
return {
|
|
42
54
|
ok: false,
|
|
55
|
+
connected: false,
|
|
56
|
+
browserErrorCode: 'not_connected',
|
|
43
57
|
error: `TheGitAI is not connected to a browser. ${availability.hint ?? ''}`.trim(),
|
|
44
58
|
failureCategory: 'external_service',
|
|
45
|
-
failureDetails: {
|
|
46
|
-
category: 'external_service',
|
|
47
|
-
tool: 'browser',
|
|
48
|
-
action: 'Report the connection instructions. The extension is a separate installation. Do not retry in a loop or invent a download URL.',
|
|
49
|
-
},
|
|
50
59
|
};
|
|
51
60
|
}
|
|
52
|
-
function bridgeFailure(error
|
|
61
|
+
function bridgeFailure(error) {
|
|
53
62
|
const code = error instanceof BrowserBridgeError ? error.code : 'browser_error';
|
|
54
63
|
const message = String(error?.message ?? error);
|
|
55
64
|
return {
|
|
@@ -57,15 +66,6 @@ function bridgeFailure(error, tool) {
|
|
|
57
66
|
error: message,
|
|
58
67
|
failureCategory: code === 'not_connected' ? 'external_service' : 'tool_exception',
|
|
59
68
|
browserErrorCode: code,
|
|
60
|
-
failureDetails: {
|
|
61
|
-
category: code === 'not_connected' ? 'external_service' : 'tool_exception',
|
|
62
|
-
tool,
|
|
63
|
-
action: code === 'timeout' || code === 'disconnected'
|
|
64
|
-
? 'The outcome is unknown. Read the page before repeating anything that submits, sends, buys, or deletes.'
|
|
65
|
-
: code === 'stale_refs' || code === 'unknown_ref'
|
|
66
|
-
? 'Re-read the page and use the references from that read.'
|
|
67
|
-
: 'Report what failed. Do not retry the same call more than once.',
|
|
68
|
-
},
|
|
69
69
|
};
|
|
70
70
|
}
|
|
71
71
|
async function call(context, op, args = {}, timeoutMs = READ_TIMEOUT_MS) {
|
|
@@ -106,10 +106,10 @@ function attachEvents(result) {
|
|
|
106
106
|
};
|
|
107
107
|
}
|
|
108
108
|
export async function browserGateway(context) {
|
|
109
|
+
const guard = await notConnected(context);
|
|
110
|
+
if (guard.ok === false)
|
|
111
|
+
return guard;
|
|
109
112
|
const availability = browserAvailability({ cwd: context.rootDir, env: context.env });
|
|
110
|
-
if (!availability.connected) {
|
|
111
|
-
return notConnected(context);
|
|
112
|
-
}
|
|
113
113
|
const platform = availability.browsers[0]?.platform ?? process.platform;
|
|
114
114
|
return {
|
|
115
115
|
ok: true,
|
|
@@ -125,7 +125,7 @@ export async function browserGateway(context) {
|
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
export async function browserTabs(context, args) {
|
|
128
|
-
const guard = notConnected(context);
|
|
128
|
+
const guard = await notConnected(context);
|
|
129
129
|
if (guard.ok === false)
|
|
130
130
|
return guard;
|
|
131
131
|
try {
|
|
@@ -135,11 +135,11 @@ export async function browserTabs(context, args) {
|
|
|
135
135
|
return attachEvents({ ok: true, ...result });
|
|
136
136
|
}
|
|
137
137
|
catch (error) {
|
|
138
|
-
return bridgeFailure(error
|
|
138
|
+
return bridgeFailure(error);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
export async function browserTabNew(context) {
|
|
142
|
-
const guard = notConnected(context);
|
|
142
|
+
const guard = await notConnected(context);
|
|
143
143
|
if (guard.ok === false)
|
|
144
144
|
return guard;
|
|
145
145
|
try {
|
|
@@ -147,11 +147,11 @@ export async function browserTabNew(context) {
|
|
|
147
147
|
return attachEvents({ ok: true, ...result });
|
|
148
148
|
}
|
|
149
149
|
catch (error) {
|
|
150
|
-
return bridgeFailure(error
|
|
150
|
+
return bridgeFailure(error);
|
|
151
151
|
}
|
|
152
152
|
}
|
|
153
153
|
export async function browserTabClose(context, args) {
|
|
154
|
-
const guard = notConnected(context);
|
|
154
|
+
const guard = await notConnected(context);
|
|
155
155
|
if (guard.ok === false)
|
|
156
156
|
return guard;
|
|
157
157
|
const tabId = Number(args.tabId ?? args.tab_id);
|
|
@@ -162,11 +162,11 @@ export async function browserTabClose(context, args) {
|
|
|
162
162
|
return attachEvents({ ok: true, ...result });
|
|
163
163
|
}
|
|
164
164
|
catch (error) {
|
|
165
|
-
return bridgeFailure(error
|
|
165
|
+
return bridgeFailure(error);
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
export async function browserNavigate(context, args) {
|
|
169
|
-
const guard = notConnected(context);
|
|
169
|
+
const guard = await notConnected(context);
|
|
170
170
|
if (guard.ok === false)
|
|
171
171
|
return guard;
|
|
172
172
|
const url = String(args.url ?? '').trim();
|
|
@@ -195,11 +195,11 @@ export async function browserNavigate(context, args) {
|
|
|
195
195
|
return attachEvents({ ok: true, ...result });
|
|
196
196
|
}
|
|
197
197
|
catch (error) {
|
|
198
|
-
return bridgeFailure(error
|
|
198
|
+
return bridgeFailure(error);
|
|
199
199
|
}
|
|
200
200
|
}
|
|
201
201
|
async function gatedTabCall(context, { tool, op, args, summary, timeoutMs = READ_TIMEOUT_MS, }) {
|
|
202
|
-
const guard = notConnected(context);
|
|
202
|
+
const guard = await notConnected(context);
|
|
203
203
|
if (guard.ok === false)
|
|
204
204
|
return guard;
|
|
205
205
|
const tabId = Number(args.tabId ?? args.tab_id);
|
|
@@ -232,7 +232,7 @@ async function gatedTabCall(context, { tool, op, args, summary, timeoutMs = READ
|
|
|
232
232
|
return attachEvents({ ok: true, tabId, url: tab.url, ...result });
|
|
233
233
|
}
|
|
234
234
|
catch (error) {
|
|
235
|
-
return bridgeFailure(error
|
|
235
|
+
return bridgeFailure(error);
|
|
236
236
|
}
|
|
237
237
|
}
|
|
238
238
|
export function browserReadPage(context, args) {
|
|
@@ -389,7 +389,7 @@ export async function browserDownloads(context, args) {
|
|
|
389
389
|
error: `Plan mode blocked browser_downloads ${action}. Listing downloads is allowed; saving or cancelling one changes the user's disk. Switch to Default mode to decide on a download.`,
|
|
390
390
|
};
|
|
391
391
|
}
|
|
392
|
-
const guard = notConnected(context);
|
|
392
|
+
const guard = await notConnected(context);
|
|
393
393
|
if (guard.ok === false)
|
|
394
394
|
return guard;
|
|
395
395
|
if (action === 'list') {
|
|
@@ -459,7 +459,7 @@ export function browserResize(context, args) {
|
|
|
459
459
|
});
|
|
460
460
|
}
|
|
461
461
|
export async function browserFileUpload(context, args) {
|
|
462
|
-
const guard = notConnected(context);
|
|
462
|
+
const guard = await notConnected(context);
|
|
463
463
|
if (guard.ok === false)
|
|
464
464
|
return guard;
|
|
465
465
|
const ref = String(args.ref ?? '').trim();
|
|
@@ -540,7 +540,7 @@ export async function browserFileUpload(context, args) {
|
|
|
540
540
|
return attachEvents({ ok: true, ...result, files: files.map((file) => file.name) });
|
|
541
541
|
}
|
|
542
542
|
catch (error) {
|
|
543
|
-
return bridgeFailure(error
|
|
543
|
+
return bridgeFailure(error);
|
|
544
544
|
}
|
|
545
545
|
}
|
|
546
546
|
function guessMimeType(file) {
|
|
@@ -580,7 +580,7 @@ const BATCH_TOOLS = {
|
|
|
580
580
|
browser_downloads: browserDownloads,
|
|
581
581
|
};
|
|
582
582
|
export async function browserBatch(context, args) {
|
|
583
|
-
const guard = notConnected(context);
|
|
583
|
+
const guard = await notConnected(context);
|
|
584
584
|
if (guard.ok === false)
|
|
585
585
|
return guard;
|
|
586
586
|
const actions = Array.isArray(args.actions) ? args.actions : [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thegitai/cli",
|
|
3
|
-
"version": "1.0.0-preview.
|
|
3
|
+
"version": "1.0.0-preview.48",
|
|
4
4
|
"description": "TheGitAI is an agentic AI coding tool for your terminal. It reads and searches your repository, writes and edits files, runs your tests, and verifies the change before handing it back.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agentic-ai",
|
|
@@ -44,11 +44,11 @@
|
|
|
44
44
|
"@lydell/node-pty-linux-x64": "1.1.0",
|
|
45
45
|
"@lydell/node-pty-win32-arm64": "1.1.0",
|
|
46
46
|
"@lydell/node-pty-win32-x64": "1.1.0",
|
|
47
|
-
"@thegitai/tui-darwin-arm64": "1.0.0-preview.
|
|
48
|
-
"@thegitai/tui-darwin-x64": "1.0.0-preview.
|
|
49
|
-
"@thegitai/tui-linux-arm64": "1.0.0-preview.
|
|
50
|
-
"@thegitai/tui-linux-x64": "1.0.0-preview.
|
|
51
|
-
"@thegitai/tui-win32-x64": "1.0.0-preview.
|
|
47
|
+
"@thegitai/tui-darwin-arm64": "1.0.0-preview.48",
|
|
48
|
+
"@thegitai/tui-darwin-x64": "1.0.0-preview.48",
|
|
49
|
+
"@thegitai/tui-linux-arm64": "1.0.0-preview.48",
|
|
50
|
+
"@thegitai/tui-linux-x64": "1.0.0-preview.48",
|
|
51
|
+
"@thegitai/tui-win32-x64": "1.0.0-preview.48",
|
|
52
52
|
"@vscode/ripgrep": "1.18.0"
|
|
53
53
|
},
|
|
54
54
|
"repository": {
|