finch-ego-lite 0.1.3 → 0.1.5

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 CHANGED
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
7
7
  var execFileAsync = promisify(execFile);
8
8
  var WEBSITE = "https://lite.ego.app/";
9
9
  var STATUS_MARKER = "__EGO_STATUS__";
10
+ var OFFLINE_CONFIRMATIONS = 2;
10
11
  var commandQueue = Promise.resolve();
11
12
  function egoBinary() {
12
13
  const local = join(homedir(), ".local", "bin", "ego-browser");
@@ -15,12 +16,18 @@ function egoBinary() {
15
16
  }
16
17
  async function processRunning() {
17
18
  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
- }
19
+ const probe = async () => {
20
+ try {
21
+ await execFileAsync("pgrep", ["-f", "/ego lite.app/Contents/MacOS/ego lite"], { timeout: 2e3 });
22
+ return true;
23
+ } catch {
24
+ return false;
25
+ }
26
+ };
27
+ const first = await probe();
28
+ if (first) return true;
29
+ await new Promise((resolve) => setTimeout(resolve, 300));
30
+ return probe();
24
31
  }
25
32
  async function runCommand(file, args, timeout = 2e4) {
26
33
  const execute = () => new Promise((resolve, reject) => {
@@ -50,6 +57,26 @@ async function installedVersion(binary) {
50
57
  return void 0;
51
58
  }
52
59
  }
60
+ var VERSION_TTL = 5 * 6e4;
61
+ var cachedVersion;
62
+ async function probeVersion(binary) {
63
+ if (cachedVersion && Date.now() - cachedVersion.at < VERSION_TTL) return cachedVersion.value;
64
+ const value = await installedVersion(binary);
65
+ if (value) cachedVersion = { value, at: Date.now() };
66
+ return value;
67
+ }
68
+ async function probeInstall(binary, running) {
69
+ const version = running && cachedVersion?.value || await probeVersion(binary);
70
+ return { installed: running || Boolean(version), version };
71
+ }
72
+ async function waitForRunning(deadlineMs = 12e3) {
73
+ const until = Date.now() + deadlineMs;
74
+ for (; ; ) {
75
+ if (await processRunning()) return true;
76
+ if (Date.now() >= until) return false;
77
+ await new Promise((resolve) => setTimeout(resolve, 400));
78
+ }
79
+ }
53
80
  function statusScript(detailed) {
54
81
  return `
55
82
  const spaces = await listTaskSpaces();
@@ -92,11 +119,10 @@ async function getStatus(detailed = false) {
92
119
  return { supported: false, installed: false, running: false, spaces: [], pageCount: 0 };
93
120
  }
94
121
  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
122
  const running = await processRunning();
99
- if (!running) return { supported: true, installed: true, running: false, version, spaces: [], pageCount: 0 };
123
+ const { installed, version } = await probeInstall(binary, running);
124
+ const base = { supported: true, installed, running, version, spaces: [], pageCount: 0 };
125
+ if (!running || !version) return base;
100
126
  try {
101
127
  const output = await runCommand(binary, ["nodejs", "-e", statusScript(detailed)], 1e4);
102
128
  const spaces = parseSpaces(output);
@@ -104,23 +130,39 @@ async function getStatus(detailed = false) {
104
130
  if (space.tabs) return sum + space.tabs.length;
105
131
  return sum + (space.recentTabTitles?.length ?? 0);
106
132
  }, 0);
107
- return { supported: true, installed: true, running: true, version, spaces, pageCount };
133
+ return { ...base, spaces, pageCount };
108
134
  } catch (error) {
109
135
  return {
110
- supported: true,
111
- installed: true,
112
- running: true,
113
- version,
114
- spaces: [],
115
- pageCount: 0,
136
+ ...base,
116
137
  error: error instanceof Error ? error.message : String(error)
117
138
  };
118
139
  }
119
140
  }
141
+ async function probeStatus(previous) {
142
+ if (process.platform !== "darwin") {
143
+ return { supported: false, installed: false, running: false, spaces: [], pageCount: 0 };
144
+ }
145
+ const running = await processRunning();
146
+ const { installed, version } = await probeInstall(egoBinary(), running);
147
+ return {
148
+ supported: true,
149
+ installed,
150
+ running,
151
+ version: version ?? previous?.version,
152
+ // A cached space list is meaningless once the process is gone.
153
+ spaces: running ? previous?.spaces ?? [] : [],
154
+ pageCount: running ? previous?.pageCount ?? 0 : 0
155
+ };
156
+ }
120
157
  async function launchEgo() {
121
158
  if (process.platform !== "darwin") throw new Error("Ego Lite currently supports macOS only.");
122
159
  await execFileAsync("open", ["-a", "ego lite"], { timeout: 5e3 });
123
160
  }
161
+ async function focusEgo() {
162
+ if (process.platform !== "darwin") return;
163
+ await execFileAsync("open", ["-a", "ego lite"], { timeout: 5e3 }).catch(() => {
164
+ });
165
+ }
124
166
  async function openWebsite() {
125
167
  const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
126
168
  const args = process.platform === "win32" ? ["/c", "start", "", WEBSITE] : [WEBSITE];
@@ -136,6 +178,7 @@ var ACTION_STAGES = {
136
178
  press_key: "acting",
137
179
  scroll: "scrolling",
138
180
  screenshot: "shooting",
181
+ switch_tab: "switching",
139
182
  close_space: "closing",
140
183
  run: "running"
141
184
  };
@@ -159,7 +202,14 @@ function buildScript(action, input, spaceId) {
159
202
  case "open_url": {
160
203
  const name = input.space_name || "web task";
161
204
  const timeout = input.timeout_seconds ?? 20;
205
+ if (spaceId !== void 0) {
206
+ return `await switchTaskSpace(${spaceId});
207
+ await openOrReuseTab(${q(input.url ?? "")}, { wait: true, timeout: ${timeout} });
208
+ const tab = await currentTab();
209
+ cliLog(JSON.stringify({ spaceId: ${spaceId}, url: tab.url, title: tab.title }));`;
210
+ }
162
211
  return `const space = await useOrCreateTaskSpace(${q(name)});
212
+ await switchTaskSpace(space.id);
163
213
  await openOrReuseTab(${q(input.url ?? "")}, { wait: true, timeout: ${timeout} });
164
214
  const tab = await currentTab();
165
215
  cliLog(JSON.stringify({ spaceId: space.id, url: tab.url, title: tab.title }));`;
@@ -206,6 +256,12 @@ cliLog(JSON.stringify({ ok: true, sy: info.sy }));`;
206
256
  return `${spaceLine}
207
257
  const path = await captureScreenshot();
208
258
  cliLog('__EGO_SHOT__' + path);`;
259
+ case "switch_tab":
260
+ return `await switchTaskSpace(${spaceId});
261
+ await switchTab(${q(input.target_id ?? "")});
262
+ await waitForLoad({ timeout: 15 }).catch(() => {});
263
+ const info = await pageInfo();
264
+ cliLog(JSON.stringify({ ok: true, spaceId: ${spaceId}, url: info.url, title: info.title }));`;
209
265
  case "close_space":
210
266
  return `await completeTaskSpace(${spaceId}, { keep: false });
211
267
  cliLog('ok');`;
@@ -274,18 +330,62 @@ function activate(ctx) {
274
330
  }
275
331
  }));
276
332
  let cachedStatus;
333
+ let lastDetailAt = 0;
334
+ let offlineMisses = 0;
335
+ let recheckTimer;
277
336
  let refreshInFlight;
278
337
  let pendingDetailedRefresh = false;
279
- let refreshStatus = () => {
338
+ const isOnline = (status) => status?.installed === true && status.running === true;
339
+ const publishStatus = (next, detailed) => {
340
+ if (isOnline(cachedStatus) && !isOnline(next) && offlineMisses + 1 < OFFLINE_CONFIRMATIONS) {
341
+ offlineMisses += 1;
342
+ if (!recheckTimer) {
343
+ recheckTimer = setTimeout(() => {
344
+ recheckTimer = void 0;
345
+ void refreshStatus(false);
346
+ }, 600);
347
+ }
348
+ return;
349
+ }
350
+ const previousOnline = isOnline(cachedStatus);
351
+ const same = JSON.stringify(cachedStatus ?? null) === JSON.stringify(next);
352
+ offlineMisses = 0;
353
+ cachedStatus = next;
354
+ if (detailed) lastDetailAt = Date.now();
355
+ if (previousOnline !== isOnline(next)) {
356
+ ctx.logger.info("Ego online state changed", {
357
+ online: isOnline(next),
358
+ running: next.running,
359
+ installed: next.installed,
360
+ version: next.version,
361
+ error: next.error
362
+ });
363
+ }
364
+ if (!same) action.notifyUpdate();
365
+ };
366
+ const refreshStatus = (detailed = false) => {
367
+ if (refreshInFlight) {
368
+ if (detailed) pendingDetailedRefresh = true;
369
+ return refreshInFlight;
370
+ }
371
+ refreshInFlight = (detailed ? getStatus(true) : probeStatus(cachedStatus)).then((status) => publishStatus(status, detailed)).catch((error) => ctx.logger.warn("Ego background status refresh failed", error)).finally(() => {
372
+ refreshInFlight = void 0;
373
+ if (pendingDetailedRefresh) {
374
+ pendingDetailedRefresh = false;
375
+ refreshStatus(true);
376
+ }
377
+ });
378
+ return refreshInFlight;
280
379
  };
281
380
  const action = ctx.composerActions.register("ego-browser", {
282
381
  async getBadge() {
283
- const online = cachedStatus?.installed === true && cachedStatus.running === true && !cachedStatus.error;
284
382
  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 };
383
+ return isOnline(cachedStatus) ? { text: t("badge.online"), active: true } : { text: t("badge.offline"), active: false };
286
384
  },
287
385
  async getMenu() {
288
- refreshStatus(true);
386
+ if (cachedStatus === void 0 || Date.now() - lastDetailAt > 3e3) {
387
+ await refreshStatus(true);
388
+ }
289
389
  if (!cachedStatus) {
290
390
  return [{
291
391
  id: "__checking__",
@@ -350,6 +450,9 @@ function activate(ctx) {
350
450
  id: "__summary__",
351
451
  label: t("menu.summary.spaces", { count: status.spaces.length }),
352
452
  description: t("menu.summary.pages", { count: status.pageCount }),
453
+ // When the space read fails while Ego is up, say so here instead of
454
+ // showing an empty list with no explanation.
455
+ hoverText: status.error ? t("status.error", { error: status.error }) : void 0,
353
456
  iconName: "ext:ego-browser-icons/compass",
354
457
  disabled: true
355
458
  }, {
@@ -428,7 +531,7 @@ ${domainOf(tab.url, t("site.unknown"))}`,
428
531
  await ctx.ui.showToast({ title: t("toast.launched"), variant: "success", position: "TC" });
429
532
  cachedStatus = void 0;
430
533
  action.notifyUpdate();
431
- refreshStatus(true);
534
+ void waitForRunning().then(() => refreshStatus(true));
432
535
  return;
433
536
  }
434
537
  if (itemId.startsWith("resume:")) {
@@ -452,15 +555,15 @@ ${domainOf(tab.url, t("site.unknown"))}`,
452
555
  if (!binary || !await installedVersion(binary)) throw new Error(t("error.notInstalled"));
453
556
  const script = `const result = await completeTaskSpace(${JSON.stringify(spaceId)}, { keep: false }); cliLog(JSON.stringify(result));`;
454
557
  await runCommand(binary, ["nodejs", "-e", script], 15e3);
455
- cachedStatus = void 0;
456
558
  action.notifyUpdate();
457
- refreshStatus(true);
559
+ refreshStatus(false);
458
560
  await ctx.ui.showToast({ title: t("toast.closed"), variant: "success", position: "TC" });
459
561
  return;
460
562
  }
461
563
  if (itemId.startsWith("tab:")) {
462
564
  const [spaceId, targetId] = unkey(itemId.slice("tab:".length));
463
565
  await launchEgo();
566
+ if (!await waitForRunning()) throw new Error(t("error.launchTimeout"));
464
567
  const binary = egoBinary();
465
568
  const script = `await switchTaskSpace(${JSON.stringify(spaceId)}); await switchTab(${JSON.stringify(targetId)}); cliLog('ok');`;
466
569
  await runCommand(binary, ["nodejs", "-e", script], 1e4);
@@ -492,6 +595,7 @@ action:
492
595
  press_key \u2014 press a key: Enter, Tab, Escape, ArrowDown... (space_id?, key)
493
596
  scroll \u2014 scroll down by pixels (space_id?, delta_y?)
494
597
  screenshot \u2014 capture the page and return it as an image (space_id?)
598
+ switch_tab \u2014 switch to a task space and activate one of its pages (space_id, target_id from status)
495
599
  extract \u2014 extract innerText of a CSS selector, or the whole page (space_id?, selector?)
496
600
  close_space \u2014 close a task space (space_id)
497
601
  run \u2014 advanced: run a raw Ego Browser script (script, timeout_seconds?)
@@ -500,10 +604,11 @@ Reuse task spaces, respect user ownership, verify meaningful actions, and comple
500
604
  inputSchema: {
501
605
  type: "object",
502
606
  properties: {
503
- action: { type: "string", enum: ["status", "open_url", "snapshot", "click", "fill", "type", "press_key", "scroll", "screenshot", "extract", "close_space", "run"] },
607
+ action: { type: "string", enum: ["status", "open_url", "snapshot", "click", "fill", "type", "press_key", "scroll", "screenshot", "switch_tab", "extract", "close_space", "run"] },
504
608
  url: { type: "string", description: "URL to open (action=open_url)." },
505
609
  space_id: { type: "number", description: "Task space id from status/open_url. Optional when only one agent space exists." },
506
610
  space_name: { type: "string", description: "Semantic name for the task space (action=open_url)." },
611
+ target_id: { type: "string", description: "Tab target id from action=status (action=switch_tab)." },
507
612
  selector: { type: "string", description: "CSS selector of the target element (click/fill/type/extract)." },
508
613
  text: { type: "string", description: "Text to type (action=type)." },
509
614
  value: { type: "string", description: "Value to fill into the field (action=fill)." },
@@ -514,15 +619,14 @@ Reuse task spaces, respect user ownership, verify meaningful actions, and comple
514
619
  },
515
620
  required: ["action"]
516
621
  },
517
- risk: "high",
622
+ risk: "medium",
518
623
  defaultEnabled: true,
519
624
  async execute(input, exec) {
520
625
  const request = input;
521
626
  const actionName = request.action ?? "";
522
627
  if (actionName === "status") {
523
628
  const status = await getStatus(true);
524
- cachedStatus = status;
525
- action.notifyUpdate();
629
+ publishStatus(status, true);
526
630
  return { content: [{ type: "text", text: statusText(status, t) }], isError: !status.supported || Boolean(status.error) };
527
631
  }
528
632
  if (!ACTION_STAGES[actionName]) {
@@ -548,21 +652,24 @@ Reuse task spaces, respect user ownership, verify meaningful actions, and comple
548
652
  if (actionName === "type" && !request.text) {
549
653
  return { content: [{ type: "text", text: "action=type requires text." }], isError: true };
550
654
  }
655
+ if (actionName === "switch_tab" && !request.target_id) {
656
+ return { content: [{ type: "text", text: "action=switch_tab requires target_id from action=status." }], isError: true };
657
+ }
551
658
  if (process.platform !== "darwin") {
552
659
  return { content: [{ type: "text", text: t("status.unsupported", { platform: process.platform }) }], isError: true };
553
660
  }
554
661
  const binary = egoBinary();
555
- const version = binary ? await installedVersion(binary) : void 0;
556
- if (!binary || !version) {
662
+ const version = await probeVersion(binary);
663
+ if (!version && !await processRunning()) {
557
664
  return {
558
665
  content: [{ type: "text", text: `Ego Lite is not installed. Install it from ${WEBSITE}, finish onboarding, then retry.` }],
559
666
  isError: true
560
667
  };
561
668
  }
562
669
  let spaceId;
563
- if (actionName !== "open_url" && actionName !== "run") {
670
+ if (actionName !== "run") {
564
671
  spaceId = await resolveSpaceId(request.space_id);
565
- if (spaceId === void 0) {
672
+ if (spaceId === void 0 && actionName !== "open_url") {
566
673
  return {
567
674
  content: [{ type: "text", text: "No task space resolved. Call action=status or action=open_url first, then pass its space_id." }],
568
675
  isError: true
@@ -572,15 +679,17 @@ Reuse task spaces, respect user ownership, verify meaningful actions, and comple
572
679
  if (!await processRunning()) {
573
680
  exec.progress.report({ stage: "launching", message: pickProgress(t, "launching") });
574
681
  await launchEgo();
575
- await new Promise((resolve) => setTimeout(resolve, 1800));
682
+ if (!await waitForRunning()) {
683
+ return { content: [{ type: "text", text: t("error.launchTimeout") }], isError: true };
684
+ }
685
+ } else {
686
+ await focusEgo();
576
687
  }
577
688
  const script = buildScript(actionName, request, spaceId);
578
689
  const timeout = Math.min(300, Math.max(1, request.timeout_seconds ?? 60)) * 1e3;
579
690
  exec.progress.report({ stage: ACTION_STAGES[actionName], message: pickProgress(t, ACTION_STAGES[actionName] ?? "browsing") });
580
691
  try {
581
692
  const output = await runCommand(binary, ["nodejs", "-e", script], timeout);
582
- cachedStatus = void 0;
583
- action.notifyUpdate();
584
693
  refreshStatus(false);
585
694
  if (actionName === "screenshot") {
586
695
  const path = output.split("__EGO_SHOT__").pop()?.split("\n")[0]?.trim();
@@ -597,32 +706,14 @@ Reuse task spaces, respect user ownership, verify meaningful actions, and comple
597
706
  }
598
707
  return { content: [{ type: "text", text: output || "ok" }] };
599
708
  } catch (error) {
600
- cachedStatus = void 0;
601
- action.notifyUpdate();
602
709
  refreshStatus(false);
603
710
  const message = error instanceof Error ? error.message : String(error);
604
711
  return { content: [{ type: "text", text: message }], isError: true };
605
712
  }
606
713
  }
607
714
  }));
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
715
  refreshStatus(true);
625
- const timer = setInterval(() => refreshStatus(true), 1e4);
716
+ const timer = setInterval(() => refreshStatus(false), 5e3);
626
717
  ctx.subscriptions.push({ dispose: () => clearInterval(timer) });
627
718
  ctx.logger.info("ego-browser mini tool activated", { cached: Boolean(cachedStatus) });
628
719
  }
package/i18n/en-US.json CHANGED
@@ -95,7 +95,8 @@
95
95
  }
96
96
  },
97
97
  "error": {
98
- "notInstalled": "Ego Browser is not installed"
98
+ "notInstalled": "Ego Browser is not installed",
99
+ "launchTimeout": "Ego Lite was launched but is not ready yet. Wait a moment and try again."
99
100
  },
100
101
  "progress": {
101
102
  "launching": "Waking up Ego Browser…|Pouring a coffee for the browser…|Ego is stretching…",
@@ -104,6 +105,7 @@
104
105
  "acting": "Clicking gently…|Tapping the button…|Filling things in…",
105
106
  "scrolling": "Scrolling down…|Rolling the wheel…",
106
107
  "shooting": "Saying cheese! 📸|Freezing the moment…",
108
+ "switching": "Switching windows…|Jumping to another page…|Ego is changing its view…",
107
109
  "closing": "Tidying up…|Closing the tab circus…|Wrapping up…",
108
110
  "running": "Ego is doing its thing…|Browser magic in progress…",
109
111
  "browsing": "Agent is working in Ego Browser…"
package/i18n/zh-CN.json CHANGED
@@ -95,7 +95,8 @@
95
95
  }
96
96
  },
97
97
  "error": {
98
- "notInstalled": "Ego Browser 未安装"
98
+ "notInstalled": "Ego Browser 未安装",
99
+ "launchTimeout": "Ego Lite 已启动但还没就绪,请稍等片刻再试。"
99
100
  },
100
101
  "progress": {
101
102
  "launching": "正在唤醒 Ego 浏览器…|给浏览器泡杯咖啡…|Ego 正在伸懒腰…",
@@ -104,6 +105,7 @@
104
105
  "acting": "轻轻点了一下…|正在按下按钮…|正在填写…",
105
106
  "scrolling": "正在往下滚动…|鼠标滚轮转起来了…",
106
107
  "shooting": "咔嚓!拍照留念 📸|定格这一刻…",
108
+ "switching": "换个窗口看看…|跳到另一个页面…|Ego 正在切换视野…",
107
109
  "closing": "正在收摊…|正在收拾战场…|任务完成,整理中…",
108
110
  "running": "Ego 正在忙活…|浏览器魔法进行中…",
109
111
  "browsing": "Agent 正在操作 Ego Browser…"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "finch-ego-lite",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Let Finch browse the web in isolated Ego Lite task spaces while you follow every page it opens.",
5
5
  "author": {
6
6
  "name": "PuterJam",
@@ -2,7 +2,7 @@
2
2
  name: ego-browser
3
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
4
  metadata:
5
- version: "0.1.2"
5
+ version: "0.1.3"
6
6
  ---
7
7
 
8
8
  # Ego Browser 工具使用指南
@@ -31,6 +31,7 @@ metadata:
31
31
  | press_key | space_id?, key | 按键:Enter/Tab/Escape… |
32
32
  | scroll | space_id?, delta_y? | 向下滚动(默认 300px) |
33
33
  | screenshot | space_id? | 截图并作为图片返回 |
34
+ | switch_tab | space_id, target_id | 先切到任务空间,再激活其中某个页面(target_id 来自 status 的 tabs) |
34
35
  | extract | space_id?, selector? | 提取区域 innerText 或整页文本 |
35
36
  | close_space | space_id | 关闭任务空间 |
36
37
  | run | script, timeout_seconds? | 高级:执行原始 Ego 脚本 |