finch-ego-lite 0.1.0 → 0.1.2
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/dist/index.js +575 -451
- package/i18n/en-US.json +9 -2
- package/i18n/zh-CN.json +9 -2
- package/package.json +21 -8
- package/skills/ego-browser/SKILL.md +40 -45
package/dist/index.js
CHANGED
|
@@ -1,71 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { execFile } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
var execFileAsync = promisify(execFile);
|
|
8
|
+
var WEBSITE = "https://lite.ego.app/";
|
|
9
|
+
var STATUS_MARKER = "__EGO_STATUS__";
|
|
10
|
+
var commandQueue = Promise.resolve();
|
|
10
11
|
function egoBinary() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return 'ego-browser';
|
|
12
|
+
const local = join(homedir(), ".local", "bin", "ego-browser");
|
|
13
|
+
if (existsSync(local)) return local;
|
|
14
|
+
return "ego-browser";
|
|
15
15
|
}
|
|
16
16
|
async function processRunning() {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
17
|
+
if (process.platform !== "darwin") return false;
|
|
18
|
+
try {
|
|
19
|
+
await execFileAsync("pgrep", ["-f", "/ego lite.app/Contents/MacOS/ego lite"], { timeout: 2e3 });
|
|
20
|
+
return true;
|
|
21
|
+
} catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
26
24
|
}
|
|
27
|
-
async function runCommand(file, args, timeout =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
});
|
|
41
|
-
// ego-browser waits for stdin EOF even when a script is supplied with -e.
|
|
42
|
-
child.stdin?.end();
|
|
25
|
+
async function runCommand(file, args, timeout = 2e4) {
|
|
26
|
+
const execute = () => new Promise((resolve, reject) => {
|
|
27
|
+
const child = execFile(file, args, {
|
|
28
|
+
timeout,
|
|
29
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
30
|
+
env: { ...process.env, PATH: `${join(homedir(), ".local", "bin")}:${process.env.PATH ?? ""}` }
|
|
31
|
+
}, (error, stdout, stderr) => {
|
|
32
|
+
const output = [stdout, stderr].filter(Boolean).join("\n").trim();
|
|
33
|
+
if (error) {
|
|
34
|
+
reject(new Error(output || error.message));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
resolve(output);
|
|
43
38
|
});
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
39
|
+
child.stdin?.end();
|
|
40
|
+
});
|
|
41
|
+
const result = commandQueue.then(execute, execute);
|
|
42
|
+
commandQueue = result.then(() => void 0, () => void 0);
|
|
43
|
+
return result;
|
|
47
44
|
}
|
|
48
45
|
async function installedVersion(binary) {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
46
|
+
try {
|
|
47
|
+
const output = await runCommand(binary, ["--version"], 3e3);
|
|
48
|
+
return output.split("\n").find((line) => line.startsWith("ego-browser "))?.replace("ego-browser ", "").trim();
|
|
49
|
+
} catch {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
56
52
|
}
|
|
57
53
|
function statusScript(detailed) {
|
|
58
|
-
|
|
54
|
+
return `
|
|
59
55
|
const spaces = await listTaskSpaces();
|
|
60
56
|
let initialTab = null;
|
|
61
|
-
if (${detailed ?
|
|
57
|
+
if (${detailed ? "true" : "false"} && spaces.some(space => space.ownership === 'agent')) {
|
|
62
58
|
initialTab = await currentTab().catch(() => null);
|
|
63
59
|
}
|
|
64
60
|
let initialSpaceId = null;
|
|
65
61
|
const result = [];
|
|
66
62
|
for (const space of spaces) {
|
|
67
63
|
const item = Object.assign({}, space);
|
|
68
|
-
if (${detailed ?
|
|
64
|
+
if (${detailed ? "true" : "false"} && space.ownership === 'agent') {
|
|
69
65
|
try {
|
|
70
66
|
await switchTaskSpace(space.id);
|
|
71
67
|
item.tabs = await listTabs();
|
|
@@ -86,425 +82,553 @@ cliLog('${STATUS_MARKER}' + JSON.stringify(result));
|
|
|
86
82
|
`;
|
|
87
83
|
}
|
|
88
84
|
function parseSpaces(output) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
return JSON.parse(json);
|
|
85
|
+
const markerIndex = output.lastIndexOf(STATUS_MARKER);
|
|
86
|
+
if (markerIndex < 0) throw new Error(output || "Ego Browser did not return status data");
|
|
87
|
+
const json = output.slice(markerIndex + STATUS_MARKER.length).split("\n")[0];
|
|
88
|
+
return JSON.parse(json);
|
|
94
89
|
}
|
|
95
90
|
async function getStatus(detailed = false) {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
spaces: [],
|
|
125
|
-
pageCount: 0,
|
|
126
|
-
error: error instanceof Error ? error.message : String(error),
|
|
127
|
-
};
|
|
128
|
-
}
|
|
91
|
+
if (process.platform !== "darwin") {
|
|
92
|
+
return { supported: false, installed: false, running: false, spaces: [], pageCount: 0 };
|
|
93
|
+
}
|
|
94
|
+
const binary = egoBinary();
|
|
95
|
+
if (!binary) return { supported: true, installed: false, running: false, spaces: [], pageCount: 0 };
|
|
96
|
+
const version = await installedVersion(binary);
|
|
97
|
+
if (!version) return { supported: true, installed: false, running: false, spaces: [], pageCount: 0 };
|
|
98
|
+
const running = await processRunning();
|
|
99
|
+
if (!running) return { supported: true, installed: true, running: false, version, spaces: [], pageCount: 0 };
|
|
100
|
+
try {
|
|
101
|
+
const output = await runCommand(binary, ["nodejs", "-e", statusScript(detailed)], 1e4);
|
|
102
|
+
const spaces = parseSpaces(output);
|
|
103
|
+
const pageCount = spaces.reduce((sum, space) => {
|
|
104
|
+
if (space.tabs) return sum + space.tabs.length;
|
|
105
|
+
return sum + (space.recentTabTitles?.length ?? 0);
|
|
106
|
+
}, 0);
|
|
107
|
+
return { supported: true, installed: true, running: true, version, spaces, pageCount };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
supported: true,
|
|
111
|
+
installed: true,
|
|
112
|
+
running: true,
|
|
113
|
+
version,
|
|
114
|
+
spaces: [],
|
|
115
|
+
pageCount: 0,
|
|
116
|
+
error: error instanceof Error ? error.message : String(error)
|
|
117
|
+
};
|
|
118
|
+
}
|
|
129
119
|
}
|
|
130
120
|
async function launchEgo() {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await execFileAsync('open', ['-a', 'ego lite'], { timeout: 5000 });
|
|
121
|
+
if (process.platform !== "darwin") throw new Error("Ego Lite currently supports macOS only.");
|
|
122
|
+
await execFileAsync("open", ["-a", "ego lite"], { timeout: 5e3 });
|
|
134
123
|
}
|
|
135
124
|
async function openWebsite() {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
125
|
+
const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
|
|
126
|
+
const args = process.platform === "win32" ? ["/c", "start", "", WEBSITE] : [WEBSITE];
|
|
127
|
+
await execFileAsync(command, args, { timeout: 5e3 });
|
|
128
|
+
}
|
|
129
|
+
var ACTION_STAGES = {
|
|
130
|
+
open_url: "opening",
|
|
131
|
+
snapshot: "reading",
|
|
132
|
+
extract: "reading",
|
|
133
|
+
click: "acting",
|
|
134
|
+
fill: "acting",
|
|
135
|
+
type: "acting",
|
|
136
|
+
press_key: "acting",
|
|
137
|
+
scroll: "scrolling",
|
|
138
|
+
screenshot: "shooting",
|
|
139
|
+
close_space: "closing",
|
|
140
|
+
run: "running"
|
|
141
|
+
};
|
|
142
|
+
function pickProgress(t, stage) {
|
|
143
|
+
const pool = t(`progress.${stage}`);
|
|
144
|
+
const list = pool.split("|").map((s) => s.trim()).filter(Boolean);
|
|
145
|
+
return list[Math.floor(Math.random() * list.length)] ?? pool;
|
|
146
|
+
}
|
|
147
|
+
async function resolveSpaceId(spaceId) {
|
|
148
|
+
if (spaceId !== void 0) return spaceId;
|
|
149
|
+
const status = await getStatus(false);
|
|
150
|
+
const agentSpaces = status.spaces.filter((space) => space.ownership === "agent");
|
|
151
|
+
if (agentSpaces.length === 1) return agentSpaces[0].id;
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
function buildScript(action, input, spaceId) {
|
|
155
|
+
const q = JSON.stringify;
|
|
156
|
+
const sel = input.selector ? q(input.selector) : "null";
|
|
157
|
+
const spaceLine = spaceId !== void 0 ? `await switchTaskSpace(${spaceId});` : "";
|
|
158
|
+
switch (action) {
|
|
159
|
+
case "open_url": {
|
|
160
|
+
const name = input.space_name || "web task";
|
|
161
|
+
const timeout = input.timeout_seconds ?? 20;
|
|
162
|
+
return `const space = await useOrCreateTaskSpace(${q(name)});
|
|
163
|
+
await openOrReuseTab(${q(input.url ?? "")}, { wait: true, timeout: ${timeout} });
|
|
164
|
+
const tab = await currentTab();
|
|
165
|
+
cliLog(JSON.stringify({ spaceId: space.id, url: tab.url, title: tab.title }));`;
|
|
166
|
+
}
|
|
167
|
+
case "snapshot":
|
|
168
|
+
return `${spaceLine}
|
|
169
|
+
const text = await snapshotText();
|
|
170
|
+
cliLog(String(text));`;
|
|
171
|
+
case "extract":
|
|
172
|
+
return `${spaceLine}
|
|
173
|
+
const sel = ${sel};
|
|
174
|
+
let text;
|
|
175
|
+
if (sel) {
|
|
176
|
+
text = String(await js('(() => { const el = document.querySelector(' + JSON.stringify(sel) + '); return el ? el.innerText : ""; })()'));
|
|
177
|
+
} else {
|
|
178
|
+
text = String(await snapshotText());
|
|
179
|
+
}
|
|
180
|
+
cliLog(text);`;
|
|
181
|
+
case "click":
|
|
182
|
+
return `${spaceLine}
|
|
183
|
+
await click(${sel});
|
|
184
|
+
await waitForLoad({ timeout: 15 }).catch(() => {});
|
|
185
|
+
const info = await pageInfo();
|
|
186
|
+
cliLog(JSON.stringify({ ok: true, url: info.url, title: info.title }));`;
|
|
187
|
+
case "fill":
|
|
188
|
+
return `${spaceLine}
|
|
189
|
+
await fillInput(${sel}, ${q(input.value ?? "")});
|
|
190
|
+
cliLog('ok');`;
|
|
191
|
+
case "type":
|
|
192
|
+
return `${spaceLine}
|
|
193
|
+
${sel !== "null" ? `await click(${sel});` : ""}
|
|
194
|
+
await typeText(${q(input.text ?? "")});
|
|
195
|
+
cliLog('ok');`;
|
|
196
|
+
case "press_key":
|
|
197
|
+
return `${spaceLine}
|
|
198
|
+
await pressKey(${q(input.key ?? "Enter")});
|
|
199
|
+
cliLog('ok');`;
|
|
200
|
+
case "scroll":
|
|
201
|
+
return `${spaceLine}
|
|
202
|
+
await scrollBy({ y: ${input.delta_y ?? 300} });
|
|
203
|
+
const info = await pageInfo();
|
|
204
|
+
cliLog(JSON.stringify({ ok: true, sy: info.sy }));`;
|
|
205
|
+
case "screenshot":
|
|
206
|
+
return `${spaceLine}
|
|
207
|
+
const path = await captureScreenshot();
|
|
208
|
+
cliLog('__EGO_SHOT__' + path);`;
|
|
209
|
+
case "close_space":
|
|
210
|
+
return `await completeTaskSpace(${spaceId}, { keep: false });
|
|
211
|
+
cliLog('ok');`;
|
|
212
|
+
case "run":
|
|
213
|
+
default:
|
|
214
|
+
return input.script ?? "";
|
|
215
|
+
}
|
|
139
216
|
}
|
|
140
217
|
function key(value) {
|
|
141
|
-
|
|
218
|
+
return Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
142
219
|
}
|
|
143
220
|
function unkey(value) {
|
|
144
|
-
|
|
221
|
+
return JSON.parse(Buffer.from(value, "base64url").toString("utf8"));
|
|
145
222
|
}
|
|
146
223
|
function compactTitle(title, fallback, maxLength = 32) {
|
|
147
|
-
|
|
148
|
-
|
|
224
|
+
const chars = Array.from(title.trim() || fallback);
|
|
225
|
+
return chars.length > maxLength ? `${chars.slice(0, maxLength).join("")}\u2026` : chars.join("");
|
|
149
226
|
}
|
|
150
227
|
function domainOf(url, fallback) {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
228
|
+
try {
|
|
229
|
+
const parsed = new URL(url);
|
|
230
|
+
return `${parsed.protocol}//${parsed.hostname.replace(/^www\./, "")}`;
|
|
231
|
+
} catch {
|
|
232
|
+
return fallback;
|
|
233
|
+
}
|
|
158
234
|
}
|
|
159
235
|
function ownershipText(ownership, t) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return t('ownership.delegated');
|
|
164
|
-
return t('ownership.user');
|
|
236
|
+
if (ownership === "agent") return t("ownership.agent");
|
|
237
|
+
if (ownership === "agentDelegatedToUser") return t("ownership.delegated");
|
|
238
|
+
return t("ownership.user");
|
|
165
239
|
}
|
|
166
240
|
function statusText(status, t) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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);
|
|
241
|
+
if (!status.supported) return t("status.unsupported", { platform: process.platform });
|
|
242
|
+
if (!status.installed) return t("status.notInstalled", { website: WEBSITE });
|
|
243
|
+
if (!status.running) return t("status.notRunning", { version: status.version ?? "unknown" });
|
|
244
|
+
if (status.error) return t("status.error", { error: status.error });
|
|
245
|
+
const spaces = status.spaces.map((space) => ({
|
|
246
|
+
id: space.id,
|
|
247
|
+
name: space.name,
|
|
248
|
+
ownership: space.ownership,
|
|
249
|
+
pages: space.tabs?.length ?? space.recentTabTitles?.length ?? 0,
|
|
250
|
+
tabs: space.tabs?.map(({ targetId, title, url, active }) => ({ targetId, title, url, active }))
|
|
251
|
+
}));
|
|
252
|
+
return JSON.stringify({ ready: true, version: status.version, pageCount: status.pageCount, spaces }, null, 2);
|
|
183
253
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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;
|
|
254
|
+
function activate(ctx) {
|
|
255
|
+
const t = (key2, values) => ctx.i18n.t(key2, values);
|
|
256
|
+
ctx.subscriptions.push(ctx.icons.register("ego-browser-icons", {
|
|
257
|
+
ego: {
|
|
258
|
+
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>'
|
|
259
|
+
},
|
|
260
|
+
"bot-off": {
|
|
261
|
+
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>'
|
|
262
|
+
},
|
|
263
|
+
compass: {
|
|
264
|
+
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>'
|
|
265
|
+
},
|
|
266
|
+
bot: {
|
|
267
|
+
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>'
|
|
268
|
+
},
|
|
269
|
+
x: {
|
|
270
|
+
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>'
|
|
271
|
+
},
|
|
272
|
+
play: {
|
|
273
|
+
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>'
|
|
274
|
+
}
|
|
275
|
+
}));
|
|
276
|
+
let cachedStatus;
|
|
277
|
+
let refreshInFlight;
|
|
278
|
+
let pendingDetailedRefresh = false;
|
|
279
|
+
let refreshStatus = () => {
|
|
280
|
+
};
|
|
281
|
+
const action = ctx.composerActions.register("ego-browser", {
|
|
282
|
+
async getBadge() {
|
|
283
|
+
const online = cachedStatus?.installed === true && cachedStatus.running === true && !cachedStatus.error;
|
|
284
|
+
if (cachedStatus?.supported === false) return { text: t("badge.unsupported"), active: false };
|
|
285
|
+
return online ? { text: t("badge.online"), active: true } : { text: t("badge.offline"), active: false };
|
|
286
|
+
},
|
|
287
|
+
async getMenu() {
|
|
288
|
+
refreshStatus(true);
|
|
289
|
+
if (!cachedStatus) {
|
|
290
|
+
return [{
|
|
291
|
+
id: "__checking__",
|
|
292
|
+
label: t("menu.checking.label"),
|
|
293
|
+
description: t("menu.checking.description"),
|
|
294
|
+
iconName: "timer",
|
|
295
|
+
disabled: true
|
|
296
|
+
}, {
|
|
297
|
+
id: "__install__",
|
|
298
|
+
label: t("menu.help.label"),
|
|
299
|
+
description: "lite.ego.app",
|
|
300
|
+
iconName: "lightbulb"
|
|
301
|
+
}];
|
|
302
|
+
}
|
|
303
|
+
const status = cachedStatus;
|
|
304
|
+
if (!status.supported) {
|
|
305
|
+
return [{
|
|
306
|
+
id: "__unsupported__",
|
|
307
|
+
label: t("menu.unsupported.label"),
|
|
308
|
+
description: t("menu.unsupported.description"),
|
|
309
|
+
hoverText: t("menu.unsupported.hover", { platform: process.platform }),
|
|
310
|
+
iconName: "ext:ego-browser-icons/bot-off",
|
|
311
|
+
disabled: true
|
|
312
|
+
}, {
|
|
313
|
+
id: "__unsupported_divider__",
|
|
314
|
+
label: "",
|
|
315
|
+
separator: true
|
|
316
|
+
}, {
|
|
317
|
+
id: "__install__",
|
|
318
|
+
label: t("menu.learn.label"),
|
|
319
|
+
description: "lite.ego.app",
|
|
320
|
+
iconName: "lightbulb"
|
|
321
|
+
}];
|
|
322
|
+
}
|
|
323
|
+
if (!status.installed) {
|
|
324
|
+
return [{
|
|
325
|
+
id: "__install__",
|
|
326
|
+
label: t("menu.install.label"),
|
|
327
|
+
description: t("menu.install.description"),
|
|
328
|
+
hoverText: t("menu.install.hover"),
|
|
329
|
+
iconName: "rocket"
|
|
330
|
+
}];
|
|
331
|
+
}
|
|
332
|
+
if (!status.running) {
|
|
333
|
+
return [{
|
|
334
|
+
id: "__install__",
|
|
335
|
+
label: t("menu.help.label"),
|
|
336
|
+
description: "lite.ego.app",
|
|
337
|
+
iconName: "lightbulb"
|
|
338
|
+
}, {
|
|
339
|
+
id: "__launch_divider__",
|
|
340
|
+
label: "",
|
|
341
|
+
separator: true
|
|
342
|
+
}, {
|
|
343
|
+
id: "__launch__",
|
|
344
|
+
label: t("menu.launch.label"),
|
|
345
|
+
description: status.version,
|
|
346
|
+
iconName: "ext:ego-browser-icons/play"
|
|
347
|
+
}];
|
|
348
|
+
}
|
|
349
|
+
const items = [{
|
|
350
|
+
id: "__summary__",
|
|
351
|
+
label: t("menu.summary.spaces", { count: status.spaces.length }),
|
|
352
|
+
description: t("menu.summary.pages", { count: status.pageCount }),
|
|
353
|
+
iconName: "ext:ego-browser-icons/compass",
|
|
354
|
+
disabled: true
|
|
355
|
+
}, {
|
|
356
|
+
id: "__spaces_divider__",
|
|
357
|
+
label: "",
|
|
358
|
+
separator: true
|
|
359
|
+
}];
|
|
360
|
+
for (const space of status.spaces) {
|
|
361
|
+
const tabs = space.tabs ?? [];
|
|
362
|
+
const children = tabs.map((tab, index) => ({
|
|
363
|
+
id: `tab:${key([space.id, tab.targetId])}`,
|
|
364
|
+
label: compactTitle(tab.title, t("page.untitled")),
|
|
365
|
+
hoverText: `${tab.title || t("page.untitled")}
|
|
366
|
+
${domainOf(tab.url, t("site.unknown"))}`,
|
|
367
|
+
current: tab.active,
|
|
368
|
+
iconName: "globe",
|
|
369
|
+
group: `space-${space.id}`,
|
|
370
|
+
groupLabel: index === 0 ? ownershipText(space.ownership, t) : void 0,
|
|
371
|
+
groupMaxVisible: index === 0 ? 6 : void 0
|
|
372
|
+
}));
|
|
373
|
+
if (space.ownership === "agentDelegatedToUser") {
|
|
374
|
+
children.push({
|
|
375
|
+
id: `resume:${key(space.id)}`,
|
|
376
|
+
label: t("menu.resume.label"),
|
|
377
|
+
description: t("menu.resume.description"),
|
|
378
|
+
iconName: "message-circle"
|
|
379
|
+
});
|
|
490
380
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
.
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
381
|
+
if (children.length === 0) {
|
|
382
|
+
children.push({
|
|
383
|
+
id: `space:${key(space.id)}`,
|
|
384
|
+
label: ownershipText(space.ownership, t),
|
|
385
|
+
description: t("menu.summary.pages", { count: space.recentTabTitles?.length ?? 0 }),
|
|
386
|
+
iconName: space.ownership === "agent" ? "bot" : "users",
|
|
387
|
+
disabled: true
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
children.push({ id: `close-divider:${space.id}`, label: "", separator: true });
|
|
391
|
+
children.push({
|
|
392
|
+
id: `close:${key(space.id)}`,
|
|
393
|
+
label: t("menu.close.label"),
|
|
394
|
+
description: t("menu.close.description"),
|
|
395
|
+
iconName: "ext:ego-browser-icons/x"
|
|
503
396
|
});
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
397
|
+
items.push({
|
|
398
|
+
id: `space-menu:${space.id}`,
|
|
399
|
+
label: space.name || t("menu.space.fallback", { id: space.id }),
|
|
400
|
+
description: ownershipText(space.ownership, t),
|
|
401
|
+
iconName: space.ownership === "agent" ? "ext:ego-browser-icons/bot" : "ext:ego-browser-icons/compass",
|
|
402
|
+
children
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
if (status.spaces.length === 0) {
|
|
406
|
+
items.push({
|
|
407
|
+
id: "__empty__",
|
|
408
|
+
label: t("menu.empty.label"),
|
|
409
|
+
description: t("menu.empty.description"),
|
|
410
|
+
hoverText: t("menu.empty.hover"),
|
|
411
|
+
iconName: "ext:ego-browser-icons/bot-off"
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return items;
|
|
415
|
+
},
|
|
416
|
+
async execute(_actionContext, itemId, actions) {
|
|
417
|
+
try {
|
|
418
|
+
if (itemId === "__install__") {
|
|
419
|
+
await openWebsite();
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
if (itemId === "__empty__") {
|
|
423
|
+
await actions.composer.fill(t("composer.startPrompt"));
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (itemId === "__launch__") {
|
|
427
|
+
await launchEgo();
|
|
428
|
+
await ctx.ui.showToast({ title: t("toast.launched"), variant: "success", position: "TC" });
|
|
429
|
+
cachedStatus = void 0;
|
|
430
|
+
action.notifyUpdate();
|
|
431
|
+
refreshStatus(true);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
if (itemId.startsWith("resume:")) {
|
|
435
|
+
const spaceId = unkey(itemId.slice("resume:".length));
|
|
436
|
+
await actions.composer.fill(t("composer.resumePrompt", { id: spaceId }));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (itemId.startsWith("close:")) {
|
|
440
|
+
const spaceId = unkey(itemId.slice("close:".length));
|
|
441
|
+
const space = cachedStatus?.spaces.find((item) => item.id === spaceId);
|
|
442
|
+
const result = await ctx.ui.showConfirmDialog({
|
|
443
|
+
title: t("dialog.close.title"),
|
|
444
|
+
description: space?.name || t("menu.space.fallback", { id: spaceId }),
|
|
445
|
+
message: t("dialog.close.message", { count: space?.tabs?.length ?? space?.recentTabTitles?.length ?? 0 }),
|
|
446
|
+
confirmLabel: t("dialog.close.confirm"),
|
|
447
|
+
cancelLabel: t("dialog.close.cancel"),
|
|
448
|
+
variant: "danger"
|
|
449
|
+
});
|
|
450
|
+
if (!result.confirmed) return;
|
|
451
|
+
const binary = egoBinary();
|
|
452
|
+
if (!binary || !await installedVersion(binary)) throw new Error(t("error.notInstalled"));
|
|
453
|
+
const script = `const result = await completeTaskSpace(${JSON.stringify(spaceId)}, { keep: false }); cliLog(JSON.stringify(result));`;
|
|
454
|
+
await runCommand(binary, ["nodejs", "-e", script], 15e3);
|
|
455
|
+
cachedStatus = void 0;
|
|
456
|
+
action.notifyUpdate();
|
|
457
|
+
refreshStatus(true);
|
|
458
|
+
await ctx.ui.showToast({ title: t("toast.closed"), variant: "success", position: "TC" });
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
if (itemId.startsWith("tab:")) {
|
|
462
|
+
const [spaceId, targetId] = unkey(itemId.slice("tab:".length));
|
|
463
|
+
await launchEgo();
|
|
464
|
+
const binary = egoBinary();
|
|
465
|
+
const script = `await switchTaskSpace(${JSON.stringify(spaceId)}); await switchTab(${JSON.stringify(targetId)}); cliLog('ok');`;
|
|
466
|
+
await runCommand(binary, ["nodejs", "-e", script], 1e4);
|
|
467
|
+
}
|
|
468
|
+
} catch (error) {
|
|
469
|
+
ctx.logger.error("Ego Composer action failed", error);
|
|
470
|
+
await ctx.ui.showToast({
|
|
471
|
+
title: t("toast.actionFailed"),
|
|
472
|
+
description: error instanceof Error ? error.message : String(error),
|
|
473
|
+
variant: "error",
|
|
474
|
+
position: "TC"
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
ctx.subscriptions.push(action);
|
|
480
|
+
ctx.subscriptions.push(ctx.i18n.onDidChangeLocale(() => action.notifyUpdate()));
|
|
481
|
+
ctx.subscriptions.push(ctx.tools.register({
|
|
482
|
+
name: "ego_browser",
|
|
483
|
+
title: "Ego Browser",
|
|
484
|
+
description: `Operate Ego Browser with declarative actions. Everyday browsing needs no scripts.
|
|
485
|
+
action:
|
|
486
|
+
status \u2014 check installation, runtime, task spaces and open pages
|
|
487
|
+
open_url \u2014 open a URL in a task space (url, space_name?) and return its spaceId
|
|
488
|
+
snapshot \u2014 read the current page as text (space_id?)
|
|
489
|
+
click \u2014 click an element by CSS selector (space_id?, selector)
|
|
490
|
+
fill \u2014 fill a form field (space_id?, selector, value)
|
|
491
|
+
type \u2014 type text; pass selector to focus the field first (space_id?, selector?, text)
|
|
492
|
+
press_key \u2014 press a key: Enter, Tab, Escape, ArrowDown... (space_id?, key)
|
|
493
|
+
scroll \u2014 scroll down by pixels (space_id?, delta_y?)
|
|
494
|
+
screenshot \u2014 capture the page and return it as an image (space_id?)
|
|
495
|
+
extract \u2014 extract innerText of a CSS selector, or the whole page (space_id?, selector?)
|
|
496
|
+
close_space \u2014 close a task space (space_id)
|
|
497
|
+
run \u2014 advanced: run a raw Ego Browser script (script, timeout_seconds?)
|
|
498
|
+
Most actions accept space_id; when omitted and exactly one agent-owned task space exists, it is used.
|
|
499
|
+
Reuse task spaces, respect user ownership, verify meaningful actions, and complete spaces when done.`,
|
|
500
|
+
inputSchema: {
|
|
501
|
+
type: "object",
|
|
502
|
+
properties: {
|
|
503
|
+
action: { type: "string", enum: ["status", "open_url", "snapshot", "click", "fill", "type", "press_key", "scroll", "screenshot", "extract", "close_space", "run"] },
|
|
504
|
+
url: { type: "string", description: "URL to open (action=open_url)." },
|
|
505
|
+
space_id: { type: "number", description: "Task space id from status/open_url. Optional when only one agent space exists." },
|
|
506
|
+
space_name: { type: "string", description: "Semantic name for the task space (action=open_url)." },
|
|
507
|
+
selector: { type: "string", description: "CSS selector of the target element (click/fill/type/extract)." },
|
|
508
|
+
text: { type: "string", description: "Text to type (action=type)." },
|
|
509
|
+
value: { type: "string", description: "Value to fill into the field (action=fill)." },
|
|
510
|
+
key: { type: "string", description: "Key to press (action=press_key): Enter, Tab, Escape..." },
|
|
511
|
+
delta_y: { type: "number", description: "Pixels to scroll down (action=scroll, default 300)." },
|
|
512
|
+
script: { type: "string", description: "Raw Ego Browser script (action=run only)." },
|
|
513
|
+
timeout_seconds: { type: "number", description: "Execution timeout, 1 to 300 seconds. Default 60." }
|
|
514
|
+
},
|
|
515
|
+
required: ["action"]
|
|
516
|
+
},
|
|
517
|
+
risk: "high",
|
|
518
|
+
defaultEnabled: true,
|
|
519
|
+
async execute(input, exec) {
|
|
520
|
+
const request = input;
|
|
521
|
+
const actionName = request.action ?? "";
|
|
522
|
+
if (actionName === "status") {
|
|
523
|
+
const status = await getStatus(true);
|
|
524
|
+
cachedStatus = status;
|
|
525
|
+
action.notifyUpdate();
|
|
526
|
+
return { content: [{ type: "text", text: statusText(status, t) }], isError: !status.supported || Boolean(status.error) };
|
|
527
|
+
}
|
|
528
|
+
if (!ACTION_STAGES[actionName]) {
|
|
529
|
+
return { content: [{ type: "text", text: `Unknown action: ${actionName}.` }], isError: true };
|
|
530
|
+
}
|
|
531
|
+
if (actionName === "run") {
|
|
532
|
+
if (typeof request.script !== "string" || !request.script.trim()) {
|
|
533
|
+
return { content: [{ type: "text", text: "action=run requires a non-empty script." }], isError: true };
|
|
534
|
+
}
|
|
535
|
+
if (request.script.length > 1e5 || request.script.includes("\0")) {
|
|
536
|
+
return { content: [{ type: "text", text: "The Ego Browser script is invalid or too large." }], isError: true };
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
if (actionName === "open_url" && !request.url) {
|
|
540
|
+
return { content: [{ type: "text", text: "action=open_url requires a url." }], isError: true };
|
|
541
|
+
}
|
|
542
|
+
if ((actionName === "click" || actionName === "fill" || actionName === "extract") && !request.selector) {
|
|
543
|
+
return { content: [{ type: "text", text: `action=${actionName} requires a CSS selector.` }], isError: true };
|
|
544
|
+
}
|
|
545
|
+
if (actionName === "fill" && request.value === void 0) {
|
|
546
|
+
return { content: [{ type: "text", text: "action=fill requires a value." }], isError: true };
|
|
547
|
+
}
|
|
548
|
+
if (actionName === "type" && !request.text) {
|
|
549
|
+
return { content: [{ type: "text", text: "action=type requires text." }], isError: true };
|
|
550
|
+
}
|
|
551
|
+
if (process.platform !== "darwin") {
|
|
552
|
+
return { content: [{ type: "text", text: t("status.unsupported", { platform: process.platform }) }], isError: true };
|
|
553
|
+
}
|
|
554
|
+
const binary = egoBinary();
|
|
555
|
+
const version = binary ? await installedVersion(binary) : void 0;
|
|
556
|
+
if (!binary || !version) {
|
|
557
|
+
return {
|
|
558
|
+
content: [{ type: "text", text: `Ego Lite is not installed. Install it from ${WEBSITE}, finish onboarding, then retry.` }],
|
|
559
|
+
isError: true
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
let spaceId;
|
|
563
|
+
if (actionName !== "open_url" && actionName !== "run") {
|
|
564
|
+
spaceId = await resolveSpaceId(request.space_id);
|
|
565
|
+
if (spaceId === void 0) {
|
|
566
|
+
return {
|
|
567
|
+
content: [{ type: "text", text: "No task space resolved. Call action=status or action=open_url first, then pass its space_id." }],
|
|
568
|
+
isError: true
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (!await processRunning()) {
|
|
573
|
+
exec.progress.report({ stage: "launching", message: pickProgress(t, "launching") });
|
|
574
|
+
await launchEgo();
|
|
575
|
+
await new Promise((resolve) => setTimeout(resolve, 1800));
|
|
576
|
+
}
|
|
577
|
+
const script = buildScript(actionName, request, spaceId);
|
|
578
|
+
const timeout = Math.min(300, Math.max(1, request.timeout_seconds ?? 60)) * 1e3;
|
|
579
|
+
exec.progress.report({ stage: ACTION_STAGES[actionName], message: pickProgress(t, ACTION_STAGES[actionName] ?? "browsing") });
|
|
580
|
+
try {
|
|
581
|
+
const output = await runCommand(binary, ["nodejs", "-e", script], timeout);
|
|
582
|
+
cachedStatus = void 0;
|
|
583
|
+
action.notifyUpdate();
|
|
584
|
+
refreshStatus(false);
|
|
585
|
+
if (actionName === "screenshot") {
|
|
586
|
+
const path = output.split("__EGO_SHOT__").pop()?.split("\n")[0]?.trim();
|
|
587
|
+
if (!path || !existsSync(path)) {
|
|
588
|
+
return { content: [{ type: "text", text: output || "Screenshot failed." }], isError: true };
|
|
589
|
+
}
|
|
590
|
+
const data = readFileSync(path).toString("base64");
|
|
591
|
+
return {
|
|
592
|
+
content: [
|
|
593
|
+
{ type: "text", text: `Screenshot: ${path}` },
|
|
594
|
+
{ type: "image", data: `data:image/png;base64,${data}`, mimeType: "image/png" }
|
|
595
|
+
]
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
return { content: [{ type: "text", text: output || "ok" }] };
|
|
599
|
+
} catch (error) {
|
|
600
|
+
cachedStatus = void 0;
|
|
601
|
+
action.notifyUpdate();
|
|
602
|
+
refreshStatus(false);
|
|
603
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
604
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}));
|
|
608
|
+
refreshStatus = (detailed = false) => {
|
|
609
|
+
if (refreshInFlight) {
|
|
610
|
+
pendingDetailedRefresh ||= detailed;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
refreshInFlight = getStatus(detailed).then((status) => {
|
|
614
|
+
cachedStatus = status;
|
|
615
|
+
action.notifyUpdate();
|
|
616
|
+
}).catch((error) => ctx.logger.warn("Ego background status refresh failed", error)).finally(() => {
|
|
617
|
+
refreshInFlight = void 0;
|
|
618
|
+
if (pendingDetailedRefresh) {
|
|
619
|
+
pendingDetailedRefresh = false;
|
|
620
|
+
refreshStatus(true);
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
};
|
|
624
|
+
refreshStatus(true);
|
|
625
|
+
const timer = setInterval(() => refreshStatus(true), 1e4);
|
|
626
|
+
ctx.subscriptions.push({ dispose: () => clearInterval(timer) });
|
|
627
|
+
ctx.logger.info("ego-browser mini tool activated", { cached: Boolean(cachedStatus) });
|
|
628
|
+
}
|
|
629
|
+
function deactivate() {
|
|
509
630
|
}
|
|
510
|
-
export
|
|
631
|
+
export {
|
|
632
|
+
activate,
|
|
633
|
+
deactivate
|
|
634
|
+
};
|