@thegitai/cli 1.0.0-preview.38 → 1.0.0-preview.39
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.
- package/README.md +20 -0
- package/dist/bin/ai.js +25 -18
- package/dist/bin/browser-host.js +265 -0
- package/dist/src/agent-mode.js +10 -0
- package/dist/src/api/chat.js +5 -1
- package/dist/src/browser/bridge.js +232 -0
- package/dist/src/browser/framing.js +42 -0
- package/dist/src/browser/native-host.js +209 -0
- package/dist/src/browser/protocol.js +46 -0
- package/dist/src/browser/session-bridge.js +104 -0
- package/dist/src/client-environment.js +2 -0
- package/dist/src/permissions.js +49 -4
- package/dist/src/session.js +6 -2
- package/dist/src/tool-executor.js +1 -0
- package/dist/src/tools/browser.js +628 -0
- package/dist/src/tools/index.js +19 -0
- package/dist/src/ui/repl.js +7 -0
- package/package.json +6 -6
|
@@ -0,0 +1,209 @@
|
|
|
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 /(google[- ]?chrome|chromium|brave|msedge|microsoft edge|vivaldi)/i.test(output);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getClientStateDir } from '../client-state.js';
|
|
5
|
+
import { pruneStaleSessions, startBrowserBridge, } from './bridge.js';
|
|
6
|
+
import { chromiumProcessRunning } from './native-host.js';
|
|
7
|
+
let bridge = null;
|
|
8
|
+
let recentEvents = [];
|
|
9
|
+
const processKey = `${process.pid.toString(36)}${crypto.randomBytes(3).toString('hex')}`;
|
|
10
|
+
export function ensureBrowserBridge(options) {
|
|
11
|
+
if (bridge)
|
|
12
|
+
return bridge;
|
|
13
|
+
const env = options.env ?? process.env;
|
|
14
|
+
pruneStaleSessions(env);
|
|
15
|
+
bridge = startBrowserBridge({
|
|
16
|
+
sessionKey: processKey,
|
|
17
|
+
label: path.basename(options.cwd) || 'TheGitAI',
|
|
18
|
+
cwd: options.cwd,
|
|
19
|
+
env,
|
|
20
|
+
});
|
|
21
|
+
bridge.onEvent((event, data) => {
|
|
22
|
+
if (event === 'connected')
|
|
23
|
+
rememberBrowserExtensionSeen(env);
|
|
24
|
+
recentEvents.push({ event, data, at: Date.now() });
|
|
25
|
+
if (recentEvents.length > 50)
|
|
26
|
+
recentEvents.splice(0, recentEvents.length - 50);
|
|
27
|
+
});
|
|
28
|
+
return bridge;
|
|
29
|
+
}
|
|
30
|
+
function browserExtensionMarkerPath(env) {
|
|
31
|
+
return path.join(getClientStateDir(env), 'browser-extension-seen');
|
|
32
|
+
}
|
|
33
|
+
function rememberBrowserExtensionSeen(env) {
|
|
34
|
+
try {
|
|
35
|
+
const marker = browserExtensionMarkerPath(env);
|
|
36
|
+
if (fs.existsSync(marker))
|
|
37
|
+
return;
|
|
38
|
+
fs.mkdirSync(path.dirname(marker), { recursive: true });
|
|
39
|
+
fs.writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export function browserExtensionSeen(env = process.env) {
|
|
45
|
+
try {
|
|
46
|
+
return fs.existsSync(browserExtensionMarkerPath(env));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
export function startBrowserDiscoveryInBackground(options) {
|
|
53
|
+
setImmediate(() => {
|
|
54
|
+
try {
|
|
55
|
+
ensureBrowserBridge(options);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function currentBrowserBridge() {
|
|
62
|
+
return bridge;
|
|
63
|
+
}
|
|
64
|
+
export function releaseBrowserAfterTurn() {
|
|
65
|
+
const active = bridge;
|
|
66
|
+
if (!active?.status().connected)
|
|
67
|
+
return;
|
|
68
|
+
void active
|
|
69
|
+
.request('release', {}, { timeoutMs: 5000 })
|
|
70
|
+
.catch(() => {
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function closeBrowserBridge() {
|
|
74
|
+
bridge?.close();
|
|
75
|
+
bridge = null;
|
|
76
|
+
recentEvents = [];
|
|
77
|
+
}
|
|
78
|
+
export function drainBrowserEvents() {
|
|
79
|
+
const events = recentEvents.map(({ event, data }) => ({ event, data }));
|
|
80
|
+
recentEvents = [];
|
|
81
|
+
return events;
|
|
82
|
+
}
|
|
83
|
+
export function browserAvailability(options) {
|
|
84
|
+
const active = ensureBrowserBridge(options);
|
|
85
|
+
const status = active.status();
|
|
86
|
+
if (status.connected) {
|
|
87
|
+
return {
|
|
88
|
+
connected: true,
|
|
89
|
+
browsers: status.browsers,
|
|
90
|
+
selected: status.selected,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const hint = status.registrationErrors.length
|
|
94
|
+
? `TheGitAI could not register its browser bridge with Chrome: ${status.registrationErrors[0]}`
|
|
95
|
+
: chromiumProcessRunning()
|
|
96
|
+
? 'Chrome is running but the TheGitAI extension has not connected. Install or enable the extension, then reload it from chrome://extensions.'
|
|
97
|
+
: 'Chrome does not appear to be running. Open Chrome with the TheGitAI extension installed.';
|
|
98
|
+
return {
|
|
99
|
+
connected: false,
|
|
100
|
+
browsers: [],
|
|
101
|
+
selected: null,
|
|
102
|
+
hint,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -2,6 +2,7 @@ import { accessSync, constants, readFileSync } from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { ensureSessionScratchDir } from './scratch-dir.js';
|
|
5
|
+
import { browserExtensionSeen } from './browser/session-bridge.js';
|
|
5
6
|
const PACKAGE_MANAGER_CANDIDATES = [
|
|
6
7
|
'apt',
|
|
7
8
|
'apt-get',
|
|
@@ -125,5 +126,6 @@ export function collectClientEnvironment(options = {}) {
|
|
|
125
126
|
...linuxDistro,
|
|
126
127
|
packageManagers: detectPackageManagers(env, platform, executableExists),
|
|
127
128
|
scratchDir: options.scratchDir ?? ensureSessionScratchDir(),
|
|
129
|
+
browserExtensionSeen: options.browserExtensionSeen ?? browserExtensionSeen(env),
|
|
128
130
|
};
|
|
129
131
|
}
|
package/dist/src/permissions.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getUnquotedShellText } from './agent-mode.js';
|
|
2
|
-
export const PERMISSION_BUCKETS = ['create', 'edit', 'delete', 'run'];
|
|
2
|
+
export const PERMISSION_BUCKETS = ['create', 'edit', 'delete', 'run', 'browse'];
|
|
3
3
|
export function createSessionGrants() {
|
|
4
|
-
return { buckets: [], commandPrefixes: [] };
|
|
4
|
+
return { buckets: [], commandPrefixes: [], origins: [] };
|
|
5
5
|
}
|
|
6
6
|
export function bucketActionLabel(bucket) {
|
|
7
7
|
if (bucket === 'create')
|
|
@@ -10,8 +10,37 @@ export function bucketActionLabel(bucket) {
|
|
|
10
10
|
return 'edit files';
|
|
11
11
|
if (bucket === 'delete')
|
|
12
12
|
return 'delete files';
|
|
13
|
+
if (bucket === 'browse')
|
|
14
|
+
return 'act on any website';
|
|
13
15
|
return 'run commands';
|
|
14
16
|
}
|
|
17
|
+
export function originForUrl(url) {
|
|
18
|
+
const raw = String(url ?? '').trim();
|
|
19
|
+
if (!raw)
|
|
20
|
+
return null;
|
|
21
|
+
try {
|
|
22
|
+
const parsed = new URL(/^[a-z][a-z0-9+.-]*:/i.test(raw) ? raw : `https://${raw}`);
|
|
23
|
+
if (parsed.protocol === 'about:' || parsed.protocol === 'chrome:')
|
|
24
|
+
return null;
|
|
25
|
+
return parsed.origin;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
export function isOriginGranted(grants, origin) {
|
|
32
|
+
if (!grants || !origin)
|
|
33
|
+
return false;
|
|
34
|
+
return grants.origins?.includes(origin) === true;
|
|
35
|
+
}
|
|
36
|
+
export function grantOrigin(grants, origin) {
|
|
37
|
+
const normalized = String(origin ?? '').trim();
|
|
38
|
+
if (!normalized)
|
|
39
|
+
return;
|
|
40
|
+
const origins = grants.origins ??= [];
|
|
41
|
+
if (!origins.includes(normalized))
|
|
42
|
+
origins.push(normalized);
|
|
43
|
+
}
|
|
15
44
|
const NO_PREFIX_GRANT_BINARIES = new Set([
|
|
16
45
|
'rm',
|
|
17
46
|
'rmdir',
|
|
@@ -164,7 +193,7 @@ export function grantCommandPrefix(grants, prefix) {
|
|
|
164
193
|
grants.commandPrefixes.push(normalized);
|
|
165
194
|
}
|
|
166
195
|
}
|
|
167
|
-
export function buildPermissionOptions(bucket, command) {
|
|
196
|
+
export function buildPermissionOptions(bucket, command, origin) {
|
|
168
197
|
const options = [
|
|
169
198
|
{ label: 'Approve once', decision: { kind: 'once' } },
|
|
170
199
|
];
|
|
@@ -175,6 +204,12 @@ export function buildPermissionOptions(bucket, command) {
|
|
|
175
204
|
decision: { kind: 'always-prefix', prefix },
|
|
176
205
|
});
|
|
177
206
|
}
|
|
207
|
+
if (bucket === 'browse' && origin) {
|
|
208
|
+
options.push({
|
|
209
|
+
label: `Allow this site for the session: ${origin}`,
|
|
210
|
+
decision: { kind: 'always-origin', origin },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
178
213
|
options.push({
|
|
179
214
|
label: `Always allow: ${bucketActionLabel(bucket)}`,
|
|
180
215
|
decision: { kind: 'always-bucket' },
|
|
@@ -192,6 +227,9 @@ function declinedSubject(bucket) {
|
|
|
192
227
|
if (bucket === 'delete') {
|
|
193
228
|
return { noun: 'file deletion', effect: 'Nothing was deleted' };
|
|
194
229
|
}
|
|
230
|
+
if (bucket === 'browse') {
|
|
231
|
+
return { noun: 'browser action', effect: 'The page was not touched' };
|
|
232
|
+
}
|
|
195
233
|
return { noun: 'edit', effect: 'Nothing was changed' };
|
|
196
234
|
}
|
|
197
235
|
function declinedResponse(bucket, toolName, extra) {
|
|
@@ -219,6 +257,10 @@ export async function ensurePermission(context, request, toolName, extra = {}) {
|
|
|
219
257
|
isCommandGranted(context.grants, request.command)) {
|
|
220
258
|
return null;
|
|
221
259
|
}
|
|
260
|
+
if (request.bucket === 'browse' &&
|
|
261
|
+
isOriginGranted(context.grants, request.origin ?? null)) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
222
264
|
if (!context.requestPermission) {
|
|
223
265
|
return {
|
|
224
266
|
ok: false,
|
|
@@ -226,7 +268,7 @@ export async function ensurePermission(context, request, toolName, extra = {}) {
|
|
|
226
268
|
error: `requestPermission is required when autoYes is false (${toolName})`,
|
|
227
269
|
};
|
|
228
270
|
}
|
|
229
|
-
const options = buildPermissionOptions(request.bucket, request.command);
|
|
271
|
+
const options = buildPermissionOptions(request.bucket, request.command, request.origin);
|
|
230
272
|
const decision = await context.requestPermission({ ...request, options });
|
|
231
273
|
if (decision.kind === 'deny') {
|
|
232
274
|
return declinedResponse(request.bucket, toolName, extra);
|
|
@@ -238,6 +280,9 @@ export async function ensurePermission(context, request, toolName, extra = {}) {
|
|
|
238
280
|
else if (decision.kind === 'always-prefix') {
|
|
239
281
|
grantCommandPrefix(context.grants, decision.prefix);
|
|
240
282
|
}
|
|
283
|
+
else if (decision.kind === 'always-origin') {
|
|
284
|
+
grantOrigin(context.grants, decision.origin);
|
|
285
|
+
}
|
|
241
286
|
}
|
|
242
287
|
return null;
|
|
243
288
|
}
|
package/dist/src/session.js
CHANGED
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { normalizeAgentMode, } from './agent-mode.js';
|
|
3
3
|
import { createSessionGrants, } from './permissions.js';
|
|
4
4
|
import { createSessionSafetyState, } from './session-safety.js';
|
|
5
|
+
import { closeBrowserBridge } from './browser/session-bridge.js';
|
|
5
6
|
import { clampInteger } from './utils.js';
|
|
6
7
|
const DEFAULT_MAX_TOOL_STEPS = 32;
|
|
7
8
|
function defaultStatus(message) {
|
|
@@ -32,7 +33,7 @@ function preserveProviderSelection(serverState) {
|
|
|
32
33
|
: null;
|
|
33
34
|
return providerSelection ? { providerSelection } : {};
|
|
34
35
|
}
|
|
35
|
-
export function createSession({ rootDir, autoYes = false, agentMode, modelId, maxToolSteps = DEFAULT_MAX_TOOL_STEPS, requestPermission = null, requestSudoPassword = null, requestUserInput = null, onStatus = null, onContextLog = null, onToolEvent = null, env = process.env, sessionId = createSessionId(), sessionName = null, history = [], serverState = null, editJournal = [], stickyFilePaths = [], editCounter = 0, safety = createSessionSafetyState(), }) {
|
|
36
|
+
export function createSession({ rootDir, autoYes = false, agentMode, modelId, maxToolSteps = DEFAULT_MAX_TOOL_STEPS, maxToolStepsWasExplicit = false, requestPermission = null, requestSudoPassword = null, requestUserInput = null, onStatus = null, onContextLog = null, onToolEvent = null, env = process.env, sessionId = createSessionId(), sessionName = null, history = [], serverState = null, editJournal = [], stickyFilePaths = [], editCounter = 0, safety = createSessionSafetyState(), }) {
|
|
36
37
|
const createdAt = new Date().toISOString();
|
|
37
38
|
const initialAgentMode = normalizeAgentMode(agentMode ?? (autoYes ? 'auto-accept' : 'default'));
|
|
38
39
|
return {
|
|
@@ -41,6 +42,7 @@ export function createSession({ rootDir, autoYes = false, agentMode, modelId, ma
|
|
|
41
42
|
autoYes: initialAgentMode === 'auto-accept',
|
|
42
43
|
agentMode: initialAgentMode,
|
|
43
44
|
maxToolSteps: clampInteger(maxToolSteps, DEFAULT_MAX_TOOL_STEPS, 128),
|
|
45
|
+
maxToolStepsWasExplicit: maxToolStepsWasExplicit === true,
|
|
44
46
|
onStatus: onStatus ?? defaultStatus,
|
|
45
47
|
onContextLog: onContextLog ?? defaultContextLog,
|
|
46
48
|
onToolEvent,
|
|
@@ -97,7 +99,9 @@ export function clearConversation(session) {
|
|
|
97
99
|
safety: createSessionSafetyState(),
|
|
98
100
|
};
|
|
99
101
|
}
|
|
100
|
-
export async function disposeSession(_session) {
|
|
102
|
+
export async function disposeSession(_session) {
|
|
103
|
+
closeBrowserBridge();
|
|
104
|
+
}
|
|
101
105
|
export function switchModel(session, modelId) {
|
|
102
106
|
session.modelId = Number(modelId);
|
|
103
107
|
return { id: session.modelId };
|
|
@@ -244,6 +244,7 @@ export async function executeLocalToolCall(session, call) {
|
|
|
244
244
|
rootDir: session.rootDir,
|
|
245
245
|
sessionId: session.sessionId,
|
|
246
246
|
autoYes: session.autoYes,
|
|
247
|
+
agentMode: session.agentMode,
|
|
247
248
|
grants: session.grants,
|
|
248
249
|
requestPermission: session.requestPermission,
|
|
249
250
|
requestSudoPassword: session.requestSudoPassword,
|