finch-ego-lite 0.1.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.
package/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # Ego Browser for Finch
2
+
3
+ Let Finch work directly on the web while you stay aware of every page the Agent opens and every task space it uses.
4
+
5
+ ## What you can do
6
+
7
+ After enabling this mini tool, simply ask Finch to:
8
+
9
+ - “Open Taobao and check the current price of a Switch game.”
10
+ - “Research this product on Google and summarize the key findings.”
11
+ - “Check my store orders and pause when you need my confirmation.”
12
+ - “Fill in this web form, but let me review it before submitting.”
13
+
14
+ Finch opens websites in isolated Ego Lite task spaces. You do not need to learn browser automation or copy page content into the conversation manually.
15
+
16
+ ## Follow the Agent’s work
17
+
18
+ The Ego button beside the Composer shows the current connection status. Open its menu to:
19
+
20
+ - see active task spaces and page counts;
21
+ - view each page title and website;
22
+ - jump directly to any page in Ego Lite;
23
+ - tell whether the Agent is working, waiting for you, or under user control;
24
+ - close a completed task space and all pages inside it.
25
+
26
+ When a task space contains many pages, the menu shows up to six at a time and lets you scroll through the rest.
27
+
28
+ ## Take control when needed
29
+
30
+ For sign-in, CAPTCHA, payment confirmation, or other sensitive steps, the Agent can pause and hand the task space to you. When finished, choose “Let Agent continue” and Finch will resume from the same page.
31
+
32
+ Task spaces are isolated from your everyday browser windows while still being able to reuse your existing login state. The Agent does not compete with the pages you are currently using, and you can inspect or take over its workspace at any time.
33
+
34
+ ## Get started
35
+
36
+ 1. Install and launch [Ego Lite](https://lite.ego.app/) on macOS. Ego Lite is a browser designed for collaboration between people and AI Agents, and provides the web environment used by this mini tool.
37
+ 2. Enable this mini tool in Finch.
38
+ 3. Tell Finch which website to visit and what you want done.
39
+ 4. Use the Ego button beside the Composer to follow progress, open pages, or take control.
40
+
41
+ > Ego Lite currently supports macOS only. On Windows or Linux, this mini tool explains that the platform is not supported and provides a link to the Ego Lite website for future updates.
42
+
43
+ ## About Finch
44
+
45
+ [Finch](https://finchwork.app/) is a lightweight, easy-to-use desktop AI Agent. It combines conversation, memory, local files, Skills, and mini tools to turn natural-language ideas into real results.
46
+
47
+ Ego Browser for Finch gives Finch observable, interruptible web access: the Agent explores and acts, while you retain the ability to inspect and confirm its work.
48
+
49
+ ---
50
+
51
+ # Ego Browser for Finch 中文说明
52
+
53
+ 让 Finch 真正走进网页完成任务,同时让你随时知道 Agent 正在看什么、做什么。
54
+
55
+ ## 它能帮你做什么
56
+
57
+ 启用后,你可以直接对 Finch 说:
58
+
59
+ - “打开淘宝,帮我看看 Switch 游戏现在多少钱。”
60
+ - “去 Google 调研这个产品,把重要结论整理出来。”
61
+ - “登录后台检查订单,有需要我确认的步骤就停下来。”
62
+ - “打开这个网页,填写表单并在提交前让我检查。”
63
+
64
+ Finch 会在 Ego Lite 的独立任务空间中打开网页。你不需要学习浏览器自动化,也不用手动复制网页内容给 Agent。
65
+
66
+ ## 随时看见 Agent 的工作现场
67
+
68
+ 输入框旁的 Ego 按钮会显示当前连接状态。展开菜单后,你可以:
69
+
70
+ - 查看 Agent 正在使用的任务空间和页面数量;
71
+ - 看到每个页面的标题与所属站点;
72
+ - 点击任意页面,直接跳转到 Ego Lite 中查看;
73
+ - 分辨当前由 Agent 操作、等待你操作,还是由你控制;
74
+ - 在任务结束后关闭整个任务空间及其中的页面。
75
+
76
+ 页面较多时,菜单最多展示六项并支持滚动,不会占满屏幕。
77
+
78
+ ## 需要你操作时,控制权会交给你
79
+
80
+ 登录、验证码、支付确认或其他敏感步骤需要你亲自处理时,Agent 可以暂停并把任务空间交给你。完成后点击“让 Agent 继续”,Finch 会从同一个页面接着工作。
81
+
82
+ 任务空间与日常浏览窗口彼此独立,但可以沿用你已有的登录状态。这样 Agent 不会抢占你正在使用的浏览器页面,你也可以随时进入现场检查。
83
+
84
+ ## 开始使用
85
+
86
+ 1. 在 macOS 上安装并启动 [Ego Lite](https://lite.ego.app/)。Ego Lite 是专为人与 AI Agent 协作设计的浏览器,也是本小工具连接网页的运行环境。
87
+ 2. 在 Finch 中启用本小工具。
88
+ 3. 直接告诉 Finch 想访问的网站和要完成的事情。
89
+ 4. 通过输入框旁的 Ego 按钮查看进度、打开页面或接管操作。
90
+
91
+ > Ego Lite 目前仅支持 macOS。在 Windows 或 Linux 上,本小工具会显示平台暂不支持,并提供 Ego Lite 官网入口了解后续进展。
92
+
93
+ ## 关于 Finch
94
+
95
+ [Finch](https://finchwork.app/) 是一款轻量、易用的桌面 AI Agent。它能结合对话、记忆、本地文件、Skills 与小工具,把自然语言中的想法逐步变成真实结果。
96
+
97
+ Ego Browser for Finch 为 Finch 增加了可观察、可接管的网页操作能力:Agent 负责探索和执行,你始终保留查看与确认的权利。
package/dist/index.js ADDED
@@ -0,0 +1,510 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { promisify } from 'node:util';
6
+ const execFileAsync = promisify(execFile);
7
+ const WEBSITE = 'https://lite.ego.app/';
8
+ const STATUS_MARKER = '__EGO_STATUS__';
9
+ let commandQueue = Promise.resolve();
10
+ function egoBinary() {
11
+ const local = join(homedir(), '.local', 'bin', 'ego-browser');
12
+ if (existsSync(local))
13
+ return local;
14
+ return 'ego-browser';
15
+ }
16
+ async function processRunning() {
17
+ if (process.platform !== 'darwin')
18
+ return false;
19
+ try {
20
+ await execFileAsync('pgrep', ['-f', '/ego lite.app/Contents/MacOS/ego lite'], { timeout: 2000 });
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ async function runCommand(file, args, timeout = 20_000) {
28
+ const execute = () => new Promise((resolve, reject) => {
29
+ const child = execFile(file, args, {
30
+ timeout,
31
+ maxBuffer: 10 * 1024 * 1024,
32
+ env: { ...process.env, PATH: `${join(homedir(), '.local', 'bin')}:${process.env.PATH ?? ''}` },
33
+ }, (error, stdout, stderr) => {
34
+ const output = [stdout, stderr].filter(Boolean).join('\n').trim();
35
+ if (error) {
36
+ reject(new Error(output || error.message));
37
+ return;
38
+ }
39
+ resolve(output);
40
+ });
41
+ // ego-browser waits for stdin EOF even when a script is supplied with -e.
42
+ child.stdin?.end();
43
+ });
44
+ const result = commandQueue.then(execute, execute);
45
+ commandQueue = result.then(() => undefined, () => undefined);
46
+ return result;
47
+ }
48
+ async function installedVersion(binary) {
49
+ try {
50
+ const output = await runCommand(binary, ['--version'], 3000);
51
+ return output.split('\n').find((line) => line.startsWith('ego-browser '))?.replace('ego-browser ', '').trim();
52
+ }
53
+ catch {
54
+ return undefined;
55
+ }
56
+ }
57
+ function statusScript(detailed) {
58
+ return `
59
+ const spaces = await listTaskSpaces();
60
+ let initialTab = null;
61
+ if (${detailed ? 'true' : 'false'} && spaces.some(space => space.ownership === 'agent')) {
62
+ initialTab = await currentTab().catch(() => null);
63
+ }
64
+ let initialSpaceId = null;
65
+ const result = [];
66
+ for (const space of spaces) {
67
+ const item = Object.assign({}, space);
68
+ if (${detailed ? 'true' : 'false'} && space.ownership === 'agent') {
69
+ try {
70
+ await switchTaskSpace(space.id);
71
+ item.tabs = await listTabs();
72
+ if (initialTab && item.tabs.some(tab => tab.targetId === initialTab.targetId)) initialSpaceId = space.id;
73
+ } catch (error) {
74
+ item.tabsError = String(error && error.message ? error.message : error);
75
+ }
76
+ }
77
+ result.push(item);
78
+ }
79
+ if (initialSpaceId !== null && initialTab) {
80
+ try {
81
+ await switchTaskSpace(initialSpaceId);
82
+ await switchTab(initialTab.targetId);
83
+ } catch (error) {}
84
+ }
85
+ cliLog('${STATUS_MARKER}' + JSON.stringify(result));
86
+ `;
87
+ }
88
+ function parseSpaces(output) {
89
+ const markerIndex = output.lastIndexOf(STATUS_MARKER);
90
+ if (markerIndex < 0)
91
+ throw new Error(output || 'Ego Browser did not return status data');
92
+ const json = output.slice(markerIndex + STATUS_MARKER.length).split('\n')[0];
93
+ return JSON.parse(json);
94
+ }
95
+ async function getStatus(detailed = false) {
96
+ if (process.platform !== 'darwin') {
97
+ return { supported: false, installed: false, running: false, spaces: [], pageCount: 0 };
98
+ }
99
+ const binary = egoBinary();
100
+ if (!binary)
101
+ return { supported: true, installed: false, running: false, spaces: [], pageCount: 0 };
102
+ const version = await installedVersion(binary);
103
+ if (!version)
104
+ return { supported: true, installed: false, running: false, spaces: [], pageCount: 0 };
105
+ const running = await processRunning();
106
+ if (!running)
107
+ return { supported: true, installed: true, running: false, version, spaces: [], pageCount: 0 };
108
+ try {
109
+ const output = await runCommand(binary, ['nodejs', '-e', statusScript(detailed)], 10_000);
110
+ const spaces = parseSpaces(output);
111
+ const pageCount = spaces.reduce((sum, space) => {
112
+ if (space.tabs)
113
+ return sum + space.tabs.length;
114
+ return sum + (space.recentTabTitles?.length ?? 0);
115
+ }, 0);
116
+ return { supported: true, installed: true, running: true, version, spaces, pageCount };
117
+ }
118
+ catch (error) {
119
+ return {
120
+ supported: true,
121
+ installed: true,
122
+ running: true,
123
+ version,
124
+ spaces: [],
125
+ pageCount: 0,
126
+ error: error instanceof Error ? error.message : String(error),
127
+ };
128
+ }
129
+ }
130
+ async function launchEgo() {
131
+ if (process.platform !== 'darwin')
132
+ throw new Error('Ego Lite currently supports macOS only.');
133
+ await execFileAsync('open', ['-a', 'ego lite'], { timeout: 5000 });
134
+ }
135
+ async function openWebsite() {
136
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
137
+ const args = process.platform === 'win32' ? ['/c', 'start', '', WEBSITE] : [WEBSITE];
138
+ await execFileAsync(command, args, { timeout: 5000 });
139
+ }
140
+ function key(value) {
141
+ return Buffer.from(JSON.stringify(value)).toString('base64url');
142
+ }
143
+ function unkey(value) {
144
+ return JSON.parse(Buffer.from(value, 'base64url').toString('utf8'));
145
+ }
146
+ function compactTitle(title, fallback, maxLength = 32) {
147
+ const chars = Array.from(title.trim() || fallback);
148
+ return chars.length > maxLength ? `${chars.slice(0, maxLength).join('')}…` : chars.join('');
149
+ }
150
+ function domainOf(url, fallback) {
151
+ try {
152
+ const parsed = new URL(url);
153
+ return `${parsed.protocol}//${parsed.hostname.replace(/^www\./, '')}`;
154
+ }
155
+ catch {
156
+ return fallback;
157
+ }
158
+ }
159
+ function ownershipText(ownership, t) {
160
+ if (ownership === 'agent')
161
+ return t('ownership.agent');
162
+ if (ownership === 'agentDelegatedToUser')
163
+ return t('ownership.delegated');
164
+ return t('ownership.user');
165
+ }
166
+ function statusText(status, t) {
167
+ if (!status.supported)
168
+ return t('status.unsupported', { platform: process.platform });
169
+ if (!status.installed)
170
+ return t('status.notInstalled', { website: WEBSITE });
171
+ if (!status.running)
172
+ return t('status.notRunning', { version: status.version ?? 'unknown' });
173
+ if (status.error)
174
+ return t('status.error', { error: status.error });
175
+ const spaces = status.spaces.map((space) => ({
176
+ id: space.id,
177
+ name: space.name,
178
+ ownership: space.ownership,
179
+ pages: space.tabs?.length ?? space.recentTabTitles?.length ?? 0,
180
+ tabs: space.tabs?.map(({ targetId, title, url, active }) => ({ targetId, title, url, active })),
181
+ }));
182
+ return JSON.stringify({ ready: true, version: status.version, pageCount: status.pageCount, spaces }, null, 2);
183
+ }
184
+ export function activate(ctx) {
185
+ const t = (key, values) => ctx.i18n.t(key, values);
186
+ ctx.subscriptions.push(ctx.icons.register('ego-browser-icons', {
187
+ ego: {
188
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor"><path stroke-width=".77" d="M4.78 3.634c1.066 0 2.042.38 2.803 1.012-.421.91-.668 2.099-.668 3.384s.248 2.474.669 3.384a4.396 4.396 0 1 1-2.804-7.78Zm4.938 0c.446 0 .95.367 1.367 1.19.405.8.667 1.934.667 3.206s-.262 2.406-.667 3.206c-.416.823-.92 1.19-1.367 1.19-.38 0-.803-.266-1.179-.858A5.15 5.15 0 0 0 9.944 8.03c0-1.37-.535-2.616-1.405-3.54.376-.59.799-.856 1.179-.856Zm5.05.007c.024.015.076.06.152.184.12.196.24.505.348.921.212.826.348 1.987.348 3.284s-.136 2.459-.348 3.285c-.107.415-.229.724-.348.92a.6.6 0 0 1-.151.183.6.6 0 0 1-.15-.184c-.12-.195-.242-.504-.348-.92-.213-.825-.348-1.987-.348-3.284s.135-2.458.348-3.284c.106-.416.228-.725.347-.92a.6.6 0 0 1 .15-.185Z"/></svg>',
189
+ },
190
+ 'bot-off': {
191
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13.67 8H18a2 2 0 0 1 2 2v4.33"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M22 22 2 2"/><path d="M8 8H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h12a2 2 0 0 0 1.414-.586"/><path d="M9 13v2"/><path d="M9.67 4H12v2.33"/></svg>',
192
+ },
193
+ compass: {
194
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z"/></svg>',
195
+ },
196
+ bot: {
197
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect width="16" height="12" x="4" y="8" rx="2"/><path d="M2 14h2"/><path d="M20 14h2"/><path d="M15 13v2"/><path d="M9 13v2"/></svg>',
198
+ },
199
+ x: {
200
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>',
201
+ },
202
+ play: {
203
+ svg: '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z"/></svg>',
204
+ },
205
+ }));
206
+ let cachedStatus;
207
+ let refreshInFlight;
208
+ let pendingDetailedRefresh = false;
209
+ let refreshStatus = () => { };
210
+ const action = ctx.composerActions.register('ego-browser', {
211
+ async getBadge() {
212
+ const online = cachedStatus?.installed === true
213
+ && cachedStatus.running === true
214
+ && !cachedStatus.error;
215
+ if (cachedStatus?.supported === false)
216
+ return { text: t('badge.unsupported'), active: false };
217
+ return online
218
+ ? { text: t('badge.online'), active: true }
219
+ : { text: t('badge.offline'), active: false };
220
+ },
221
+ async getMenu() {
222
+ refreshStatus(true);
223
+ if (!cachedStatus) {
224
+ return [{
225
+ id: '__checking__',
226
+ label: t('menu.checking.label'),
227
+ description: t('menu.checking.description'),
228
+ iconName: 'timer',
229
+ disabled: true,
230
+ }, {
231
+ id: '__install__',
232
+ label: t('menu.help.label'),
233
+ description: 'lite.ego.app',
234
+ iconName: 'lightbulb',
235
+ }];
236
+ }
237
+ const status = cachedStatus;
238
+ if (!status.supported) {
239
+ return [{
240
+ id: '__unsupported__',
241
+ label: t('menu.unsupported.label'),
242
+ description: t('menu.unsupported.description'),
243
+ hoverText: t('menu.unsupported.hover', { platform: process.platform }),
244
+ iconName: 'ext:ego-browser-icons/bot-off',
245
+ disabled: true,
246
+ }, {
247
+ id: '__unsupported_divider__',
248
+ label: '',
249
+ separator: true,
250
+ }, {
251
+ id: '__install__',
252
+ label: t('menu.learn.label'),
253
+ description: 'lite.ego.app',
254
+ iconName: 'lightbulb',
255
+ }];
256
+ }
257
+ if (!status.installed) {
258
+ return [{
259
+ id: '__install__',
260
+ label: t('menu.install.label'),
261
+ description: t('menu.install.description'),
262
+ hoverText: t('menu.install.hover'),
263
+ iconName: 'rocket',
264
+ }];
265
+ }
266
+ if (!status.running) {
267
+ return [{
268
+ id: '__install__',
269
+ label: t('menu.help.label'),
270
+ description: 'lite.ego.app',
271
+ iconName: 'lightbulb',
272
+ }, {
273
+ id: '__launch_divider__',
274
+ label: '',
275
+ separator: true,
276
+ }, {
277
+ id: '__launch__',
278
+ label: t('menu.launch.label'),
279
+ description: status.version,
280
+ iconName: 'ext:ego-browser-icons/play',
281
+ }];
282
+ }
283
+ const items = [{
284
+ id: '__summary__',
285
+ label: t('menu.summary.spaces', { count: status.spaces.length }),
286
+ description: t('menu.summary.pages', { count: status.pageCount }),
287
+ iconName: 'ext:ego-browser-icons/compass',
288
+ disabled: true,
289
+ }, {
290
+ id: '__spaces_divider__',
291
+ label: '',
292
+ separator: true,
293
+ }];
294
+ for (const space of status.spaces) {
295
+ const tabs = space.tabs ?? [];
296
+ const children = tabs.map((tab, index) => ({
297
+ id: `tab:${key([space.id, tab.targetId])}`,
298
+ label: compactTitle(tab.title, t('page.untitled')),
299
+ hoverText: `${tab.title || t('page.untitled')}\n${domainOf(tab.url, t('site.unknown'))}`,
300
+ current: tab.active,
301
+ iconName: 'globe',
302
+ group: `space-${space.id}`,
303
+ groupLabel: index === 0 ? ownershipText(space.ownership, t) : undefined,
304
+ groupMaxVisible: index === 0 ? 6 : undefined,
305
+ }));
306
+ if (space.ownership === 'agentDelegatedToUser') {
307
+ children.push({
308
+ id: `resume:${key(space.id)}`,
309
+ label: t('menu.resume.label'),
310
+ description: t('menu.resume.description'),
311
+ iconName: 'message-circle',
312
+ });
313
+ }
314
+ if (children.length === 0) {
315
+ children.push({
316
+ id: `space:${key(space.id)}`,
317
+ label: ownershipText(space.ownership, t),
318
+ description: t('menu.summary.pages', { count: space.recentTabTitles?.length ?? 0 }),
319
+ iconName: space.ownership === 'agent' ? 'bot' : 'users',
320
+ disabled: true,
321
+ });
322
+ }
323
+ children.push({ id: `close-divider:${space.id}`, label: '', separator: true });
324
+ children.push({
325
+ id: `close:${key(space.id)}`,
326
+ label: t('menu.close.label'),
327
+ description: t('menu.close.description'),
328
+ iconName: 'ext:ego-browser-icons/x',
329
+ });
330
+ items.push({
331
+ id: `space-menu:${space.id}`,
332
+ label: space.name || t('menu.space.fallback', { id: space.id }),
333
+ description: ownershipText(space.ownership, t),
334
+ iconName: space.ownership === 'agent'
335
+ ? 'ext:ego-browser-icons/bot'
336
+ : 'ext:ego-browser-icons/compass',
337
+ children,
338
+ });
339
+ }
340
+ if (status.spaces.length === 0) {
341
+ items.push({
342
+ id: '__empty__',
343
+ label: t('menu.empty.label'),
344
+ description: t('menu.empty.description'),
345
+ hoverText: t('menu.empty.hover'),
346
+ iconName: 'ext:ego-browser-icons/bot-off',
347
+ });
348
+ }
349
+ return items;
350
+ },
351
+ async execute(_actionContext, itemId, actions) {
352
+ try {
353
+ if (itemId === '__install__') {
354
+ await openWebsite();
355
+ return;
356
+ }
357
+ if (itemId === '__empty__') {
358
+ await actions.composer.fill(t('composer.startPrompt'));
359
+ return;
360
+ }
361
+ if (itemId === '__launch__') {
362
+ await launchEgo();
363
+ await ctx.ui.showToast({ title: t('toast.launched'), variant: 'success', position: 'TC' });
364
+ cachedStatus = undefined;
365
+ action.notifyUpdate();
366
+ refreshStatus(true);
367
+ return;
368
+ }
369
+ if (itemId.startsWith('resume:')) {
370
+ const spaceId = unkey(itemId.slice('resume:'.length));
371
+ await actions.composer.fill(t('composer.resumePrompt', { id: spaceId }));
372
+ return;
373
+ }
374
+ if (itemId.startsWith('close:')) {
375
+ const spaceId = unkey(itemId.slice('close:'.length));
376
+ const space = cachedStatus?.spaces.find((item) => item.id === spaceId);
377
+ const result = await ctx.ui.showConfirmDialog({
378
+ title: t('dialog.close.title'),
379
+ description: space?.name || t('menu.space.fallback', { id: spaceId }),
380
+ message: t('dialog.close.message', { count: space?.tabs?.length ?? space?.recentTabTitles?.length ?? 0 }),
381
+ confirmLabel: t('dialog.close.confirm'),
382
+ cancelLabel: t('dialog.close.cancel'),
383
+ variant: 'danger',
384
+ });
385
+ if (!result.confirmed)
386
+ return;
387
+ const binary = egoBinary();
388
+ if (!binary || !(await installedVersion(binary)))
389
+ throw new Error(t('error.notInstalled'));
390
+ const script = `const result = await completeTaskSpace(${JSON.stringify(spaceId)}, { keep: false }); cliLog(JSON.stringify(result));`;
391
+ await runCommand(binary, ['nodejs', '-e', script], 15_000);
392
+ cachedStatus = undefined;
393
+ action.notifyUpdate();
394
+ refreshStatus(true);
395
+ await ctx.ui.showToast({ title: t('toast.closed'), variant: 'success', position: 'TC' });
396
+ return;
397
+ }
398
+ if (itemId.startsWith('tab:')) {
399
+ const [spaceId, targetId] = unkey(itemId.slice('tab:'.length));
400
+ await launchEgo();
401
+ const binary = egoBinary();
402
+ const script = `await switchTaskSpace(${JSON.stringify(spaceId)}); await switchTab(${JSON.stringify(targetId)}); cliLog('ok');`;
403
+ await runCommand(binary, ['nodejs', '-e', script], 10_000);
404
+ }
405
+ }
406
+ catch (error) {
407
+ ctx.logger.error('Ego Composer action failed', error);
408
+ await ctx.ui.showToast({
409
+ title: t('toast.actionFailed'),
410
+ description: error instanceof Error ? error.message : String(error),
411
+ variant: 'error',
412
+ position: 'TC',
413
+ });
414
+ }
415
+ },
416
+ });
417
+ ctx.subscriptions.push(action);
418
+ ctx.subscriptions.push(ctx.i18n.onDidChangeLocale(() => action.notifyUpdate()));
419
+ ctx.subscriptions.push(ctx.tools.register({
420
+ name: 'ego_browser',
421
+ title: 'Ego Browser',
422
+ description: `Operate Ego Browser through its preloaded JavaScript helpers.
423
+ action:
424
+ status — check installation, runtime, task-space ownership, and open pages
425
+ run — execute one coherent Ego Browser JavaScript program (requires script)
426
+ Use cliLog(...) for output. Reuse task spaces, respect user control, verify actions, and complete finished spaces.`,
427
+ inputSchema: {
428
+ type: 'object',
429
+ properties: {
430
+ action: { type: 'string', enum: ['status', 'run'] },
431
+ script: { type: 'string', description: 'JavaScript program for action=run. Ego Browser helpers are preloaded.' },
432
+ timeout_seconds: { type: 'number', description: 'Execution timeout for action=run, from 1 to 300 seconds. Default 60.' },
433
+ },
434
+ required: ['action'],
435
+ },
436
+ risk: 'high',
437
+ defaultEnabled: true,
438
+ async execute(input, exec) {
439
+ const request = input;
440
+ if (request.action === 'status') {
441
+ const status = await getStatus(true);
442
+ cachedStatus = status;
443
+ action.notifyUpdate();
444
+ return { content: [{ type: 'text', text: statusText(status, t) }], isError: !status.supported || Boolean(status.error) };
445
+ }
446
+ if (request.action !== 'run' || typeof request.script !== 'string' || !request.script.trim()) {
447
+ return { content: [{ type: 'text', text: 'action=run requires a non-empty script.' }], isError: true };
448
+ }
449
+ if (request.script.length > 100_000 || request.script.includes('\0')) {
450
+ return { content: [{ type: 'text', text: 'The Ego Browser script is invalid or too large.' }], isError: true };
451
+ }
452
+ if (process.platform !== 'darwin') {
453
+ return { content: [{ type: 'text', text: t('status.unsupported', { platform: process.platform }) }], isError: true };
454
+ }
455
+ const binary = egoBinary();
456
+ const version = binary ? await installedVersion(binary) : undefined;
457
+ if (!binary || !version) {
458
+ return {
459
+ content: [{ type: 'text', text: `Ego Lite is not installed. Install it from ${WEBSITE}, finish onboarding, then retry.` }],
460
+ isError: true,
461
+ };
462
+ }
463
+ if (!(await processRunning())) {
464
+ exec.progress.report({ stage: 'launching', message: t('progress.launching') });
465
+ await launchEgo();
466
+ await new Promise((resolve) => setTimeout(resolve, 1800));
467
+ }
468
+ const timeout = Math.min(300, Math.max(1, request.timeout_seconds ?? 60)) * 1000;
469
+ exec.progress.report({ stage: 'browsing', message: t('progress.browsing') });
470
+ try {
471
+ const output = await runCommand(binary, ['nodejs', '-e', request.script], timeout);
472
+ cachedStatus = undefined;
473
+ action.notifyUpdate();
474
+ refreshStatus(false);
475
+ return { content: [{ type: 'text', text: output || 'Ego Browser script completed.' }] };
476
+ }
477
+ catch (error) {
478
+ cachedStatus = undefined;
479
+ action.notifyUpdate();
480
+ refreshStatus(false);
481
+ const message = error instanceof Error ? error.message : String(error);
482
+ return { content: [{ type: 'text', text: message }], isError: true };
483
+ }
484
+ },
485
+ }));
486
+ refreshStatus = (detailed = false) => {
487
+ if (refreshInFlight) {
488
+ pendingDetailedRefresh ||= detailed;
489
+ return;
490
+ }
491
+ refreshInFlight = getStatus(detailed)
492
+ .then((status) => {
493
+ cachedStatus = status;
494
+ action.notifyUpdate();
495
+ })
496
+ .catch((error) => ctx.logger.warn('Ego background status refresh failed', error))
497
+ .finally(() => {
498
+ refreshInFlight = undefined;
499
+ if (pendingDetailedRefresh) {
500
+ pendingDetailedRefresh = false;
501
+ refreshStatus(true);
502
+ }
503
+ });
504
+ };
505
+ refreshStatus(true);
506
+ const timer = setInterval(() => refreshStatus(true), 10_000);
507
+ ctx.subscriptions.push({ dispose: () => clearInterval(timer) });
508
+ ctx.logger.info('ego-browser mini tool activated', { cached: Boolean(cachedStatus) });
509
+ }
510
+ export function deactivate() { }
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "Ego Browser",
3
+ "description": "Let Finch browse the web in isolated Ego Lite task spaces while you follow every page it opens.",
4
+ "systemPrompt": "Use ego_browser for browser automation. Call action=status to inspect Ego availability and task spaces. Call action=run with one coherent ego-browser JavaScript program for browser work. Reuse task spaces, respect user ownership, verify meaningful actions, and complete spaces when done.",
5
+ "composerActions": {
6
+ "ego-browser": {
7
+ "tooltip": "Ego Browser"
8
+ }
9
+ },
10
+ "toolMeta": {
11
+ "name": "Ego Browser"
12
+ },
13
+ "badge": {
14
+ "online": "Ego online",
15
+ "offline": "Ego offline",
16
+ "unsupported": "macOS only"
17
+ },
18
+ "menu": {
19
+ "checking": {
20
+ "label": "Checking Ego status…",
21
+ "description": "Updates automatically"
22
+ },
23
+ "help": {
24
+ "label": "Install and help"
25
+ },
26
+ "learn": {
27
+ "label": "Learn about Ego Lite"
28
+ },
29
+ "unsupported": {
30
+ "label": "Ego Lite requires macOS",
31
+ "description": "This device is not supported",
32
+ "hover": "Ego Lite currently runs on macOS only. Current platform: {platform}."
33
+ },
34
+ "install": {
35
+ "label": "Install Ego Lite",
36
+ "description": "Open official website",
37
+ "hover": "Finish first-time setup after installation so Ego can register the ego-browser command."
38
+ },
39
+ "launch": {
40
+ "label": "Launch Ego Lite"
41
+ },
42
+ "summary": {
43
+ "spaces": "{count} task spaces",
44
+ "pages": "{count} pages"
45
+ },
46
+ "resume": {
47
+ "label": "Let Agent continue when done",
48
+ "description": "Insert a continue message"
49
+ },
50
+ "close": {
51
+ "label": "Close task space",
52
+ "description": "Close every page inside"
53
+ },
54
+ "space": {
55
+ "fallback": "Task space {id}"
56
+ },
57
+ "empty": {
58
+ "label": "Agent is taking a break",
59
+ "description": "Click to start a browsing task",
60
+ "hover": "Tell Agent which website to visit and what you want done."
61
+ }
62
+ },
63
+ "page": {
64
+ "untitled": "Untitled page"
65
+ },
66
+ "site": {
67
+ "unknown": "Unknown site"
68
+ },
69
+ "ownership": {
70
+ "agent": "Agent working",
71
+ "delegated": "Waiting for you",
72
+ "user": "User control"
73
+ },
74
+ "status": {
75
+ "unsupported": "Ego Lite currently supports macOS only. Current platform: {platform}.",
76
+ "notInstalled": "Ego Lite is not installed. Install it from {website} and finish first-time setup.",
77
+ "notRunning": "Ego Lite {version} is installed but not running.",
78
+ "error": "Ego Lite is running, but its status could not be read: {error}"
79
+ },
80
+ "composer": {
81
+ "startPrompt": "Please use Ego Browser to visit: ",
82
+ "resumePrompt": "I finished the manual steps in Ego task space {id}. Please continue."
83
+ },
84
+ "toast": {
85
+ "launched": "Ego Lite launched",
86
+ "closed": "Task space closed",
87
+ "actionFailed": "Could not open the Ego page"
88
+ },
89
+ "dialog": {
90
+ "close": {
91
+ "title": "Close this Ego task space?",
92
+ "message": "This will close all {count} pages inside.",
93
+ "confirm": "Close",
94
+ "cancel": "Cancel"
95
+ }
96
+ },
97
+ "error": {
98
+ "notInstalled": "Ego Browser is not installed"
99
+ },
100
+ "progress": {
101
+ "launching": "Launching Ego Lite…",
102
+ "browsing": "Agent is working in Ego Browser…"
103
+ }
104
+ }
@@ -0,0 +1,104 @@
1
+ {
2
+ "name": "Ego 浏览器",
3
+ "description": "让 Finch 在独立的 Ego Lite 任务空间中浏览网页,你可以随时查看它打开的每个页面。",
4
+ "systemPrompt": "使用 ego_browser 执行浏览器自动化。先用 action=status 检查 Ego 状态和任务空间;用 action=run 执行一段完整的 Ego Browser JavaScript。复用任务空间、尊重用户控制权、验证关键操作,并在完成后关闭任务空间。",
5
+ "composerActions": {
6
+ "ego-browser": {
7
+ "tooltip": "Ego 浏览器"
8
+ }
9
+ },
10
+ "toolMeta": {
11
+ "name": "Ego 浏览器"
12
+ },
13
+ "badge": {
14
+ "online": "Ego 在线",
15
+ "offline": "Ego 离线",
16
+ "unsupported": "仅支持 macOS"
17
+ },
18
+ "menu": {
19
+ "checking": {
20
+ "label": "正在检查 Ego 状态…",
21
+ "description": "完成后自动更新"
22
+ },
23
+ "help": {
24
+ "label": "安装与帮助"
25
+ },
26
+ "learn": {
27
+ "label": "了解 Ego Lite"
28
+ },
29
+ "unsupported": {
30
+ "label": "Ego Lite 仅支持 macOS",
31
+ "description": "当前设备暂不可用",
32
+ "hover": "Ego Lite 目前只能在 macOS 上运行。当前平台:{platform}。"
33
+ },
34
+ "install": {
35
+ "label": "安装 Ego Lite",
36
+ "description": "打开官方网站",
37
+ "hover": "安装后完成首次引导,Ego 会注册 ego-browser 命令。"
38
+ },
39
+ "launch": {
40
+ "label": "启动 Ego Lite"
41
+ },
42
+ "summary": {
43
+ "spaces": "{count} 个任务空间",
44
+ "pages": "{count} 个页面"
45
+ },
46
+ "resume": {
47
+ "label": "完成后让 Agent 继续",
48
+ "description": "插入继续指令"
49
+ },
50
+ "close": {
51
+ "label": "关闭任务空间",
52
+ "description": "关闭其中全部页面"
53
+ },
54
+ "space": {
55
+ "fallback": "任务空间 {id}"
56
+ },
57
+ "empty": {
58
+ "label": "Agent 在休息中",
59
+ "description": "点击发起浏览任务",
60
+ "hover": "告诉 Agent 想访问的网站和要完成的事情。"
61
+ }
62
+ },
63
+ "page": {
64
+ "untitled": "未命名页面"
65
+ },
66
+ "site": {
67
+ "unknown": "未知站点"
68
+ },
69
+ "ownership": {
70
+ "agent": "Agent 操作中",
71
+ "delegated": "等待你操作",
72
+ "user": "用户控制"
73
+ },
74
+ "status": {
75
+ "unsupported": "Ego Lite 目前仅支持 macOS。当前平台:{platform}。",
76
+ "notInstalled": "Ego Lite 未安装。请从 {website} 安装并完成首次引导。",
77
+ "notRunning": "Ego Lite 已安装({version}),当前未运行。",
78
+ "error": "Ego Lite 正在运行,但状态读取失败:{error}"
79
+ },
80
+ "composer": {
81
+ "startPrompt": "请使用 Ego 浏览器帮我访问:",
82
+ "resumePrompt": "我已完成 Ego 任务空间 {id} 中的手动操作,请继续。"
83
+ },
84
+ "toast": {
85
+ "launched": "Ego Lite 已启动",
86
+ "closed": "任务空间已关闭",
87
+ "actionFailed": "无法打开 Ego 页面"
88
+ },
89
+ "dialog": {
90
+ "close": {
91
+ "title": "关闭 Ego 任务空间?",
92
+ "message": "将关闭其中 {count} 个页面。",
93
+ "confirm": "关闭",
94
+ "cancel": "取消"
95
+ }
96
+ },
97
+ "error": {
98
+ "notInstalled": "Ego Browser 未安装"
99
+ },
100
+ "progress": {
101
+ "launching": "正在启动 Ego Lite…",
102
+ "browsing": "Agent 正在操作 Ego Browser…"
103
+ }
104
+ }
package/icon.png ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "finch-ego-lite",
3
+ "version": "0.1.0",
4
+ "description": "Let Finch browse the web in isolated Ego Lite task spaces while you follow every page it opens.",
5
+ "author": {
6
+ "name": "Puter Jam",
7
+ "url": "https://github.com/puterjam"
8
+ },
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/puterjam/finch-tools.git",
12
+ "directory": "finch-ego-lite"
13
+ },
14
+ "homepage": "https://github.com/puterjam/finch-tools/tree/main/finch-ego-lite#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/puterjam/finch-tools/issues"
17
+ },
18
+ "license": "MIT",
19
+ "type": "module",
20
+ "main": "dist/index.js",
21
+ "scripts": {
22
+ "build": "tsc -p tsconfig.json",
23
+ "prepublishOnly": "npm run build && npx @finchtoys/minitools doctor ."
24
+ },
25
+ "finch": {
26
+ "manifestVersion": 1,
27
+ "id": "ego-browser",
28
+ "name": "Ego Browser",
29
+ "description": "Control Ego Browser and see the pages your Agent is working on.",
30
+ "systemPrompt": "Use ego_browser for browser automation. Call action=status to inspect Ego availability and task spaces. Call action=run with one coherent ego-browser JavaScript program for browser work. Reuse task spaces, respect user ownership, verify meaningful actions, and complete spaces when done.",
31
+ "main": "dist/index.js",
32
+ "activationEvents": ["onStartup"],
33
+ "miniToolType": "local",
34
+ "categories": ["productivity", "developer"],
35
+ "contributes": {
36
+ "tools": true,
37
+ "composerActions": [
38
+ {
39
+ "id": "ego-browser",
40
+ "icon": "ext:ego-browser-icons/ego",
41
+ "tooltip": "Ego Browser"
42
+ }
43
+ ],
44
+ "iconPacks": [
45
+ {
46
+ "id": "ego-browser-icons",
47
+ "label": "Ego Browser Icons"
48
+ }
49
+ ],
50
+ "skills": true
51
+ },
52
+ "permissions": {
53
+ "filesystem": "none",
54
+ "network": false,
55
+ "shell": true
56
+ }
57
+ },
58
+ "devDependencies": {
59
+ "@finchtoys/minitool-api": "^0.2.9",
60
+ "@types/node": "^26.1.0",
61
+ "typescript": "^6.0.3"
62
+ },
63
+ "files": ["dist/", "i18n/", "skills/", "icon.png", "README.md"]
64
+ }
@@ -0,0 +1,51 @@
1
+ ---
2
+ name: ego-browser
3
+ description: Use Ego Browser whenever the user needs to open a website, navigate pages, click, type, upload, take screenshots, extract page data, test web apps, log in, or automate browser actions. Ego gives Agents isolated task spaces that reuse the user's login state without competing with normal browser windows.
4
+ metadata:
5
+ version: "0.1.0"
6
+ ---
7
+
8
+ # Ego Browser
9
+
10
+ Use the `ego_browser` Agent tool instead of Bash.
11
+
12
+ ## Start
13
+
14
+ Call `ego_browser` with `action: "status"` when availability or current task spaces matter. For browser work call `action: "run"` with one coherent JavaScript program in `script`.
15
+
16
+ All Ego helpers are preloaded in the script. Print model-visible output with `cliLog(...)`.
17
+
18
+ ```js
19
+ const task = await useOrCreateTaskSpace('inspect example page')
20
+ await openOrReuseTab('https://example.com', { wait: true, timeout: 20 })
21
+ cliLog(await snapshotText())
22
+ ```
23
+
24
+ ## Main helpers
25
+
26
+ - Spaces: `listTaskSpaces`, `useOrCreateTaskSpace`, `claimTaskSpace`, `handOffTaskSpace`, `takeOverTaskSpace`, `waitForAgentControl`, `completeTaskSpace`
27
+ - Tabs: `listTabs`, `openOrReuseTab`, `closeTab`, `gotoAndWait`, `currentTab`, `switchTab`, `pageInfo`, `ensureRealTab`
28
+ - Observe: `snapshotText`, `captureScreenshot`, `drainEvents`
29
+ - Act: `click`, `doubleClick`, `hover`, `dragMouse`, `scrollBy`, `scroll`, `fillInput`, `typeText`, `pressKey`, `uploadFile`
30
+ - Wait: `wait`, `waitForLoad`, `waitForElement`, `waitForNetworkIdle`
31
+ - Advanced: `js`, `cdp`, `serverFetch`, `browserFetch`
32
+
33
+ ## Task-space rules
34
+
35
+ Reuse the same short, goal-named task space across follow-ups. Prefer its numeric `id` after creation. Respect `ownership`:
36
+
37
+ - `agent`: the Agent may act.
38
+ - `agentDelegatedToUser`: the user controls it; stop and wait.
39
+ - `user`: claim only after explicit user confirmation.
40
+
41
+ When login, captcha, or manual confirmation is required, call `handOffTaskSpace(id)` and tell the user what to do. Call `takeOverTaskSpace(id)` only after the user explicitly says to continue.
42
+
43
+ Finish in a dedicated final call with `completeTaskSpace(id, { keep: false })`. Use `keep: true` only when the user explicitly wants the live page left open or must continue manually.
44
+
45
+ ## Workflow
46
+
47
+ 1. Semantic pages: `snapshotText()` then act with `@N`, stable `loc=...`, or CSS selectors.
48
+ 2. Canvas/rich editors: screenshot, coordinates, and real keyboard input; test a tiny write first.
49
+ 3. Extraction/state: use one `js(String.raw\`(() => { ... })()\`) expression.
50
+
51
+ After meaningful actions, verify with a new snapshot, `pageInfo()`, or screenshot. Refs from `snapshotText()` are valid only for the latest snapshot. Wait and timeout values are seconds unless the name ends in `Ms`.