@cyrusxc5/better-ego-browser 0.0.0-stage → 0.3.6

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.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +109 -0
  3. package/README.md +108 -2
  4. package/THIRD_PARTY_LICENSE.txt +21 -0
  5. package/UPSTREAM.json +9 -0
  6. package/cordis.patch.yml +3 -0
  7. package/docs/demo.json +27 -0
  8. package/docs/images/dsh-ego-lite-windows-github.png +0 -0
  9. package/docs/images/ego-lite-windows-spaces.png +0 -0
  10. package/dsh/loopback/browser-action.mjs +21 -0
  11. package/dsh/loopback/server.mjs +108 -0
  12. package/dsh/plugin.mjs +20 -0
  13. package/dsh/skill.mjs +40 -0
  14. package/package.json +65 -6
  15. package/skills/ego-browser/SKILL.md +54 -0
  16. package/skills/ego-browser/agents/openai.yaml +7 -0
  17. package/skills/ego-browser/assets/composer-icon.png +0 -0
  18. package/skills/ego-browser/assets/logo.png +0 -0
  19. package/skills/ego-browser/learnings/github/browser-tools/repo-stats.js +10 -0
  20. package/skills/ego-browser/learnings/github/manifest.json +61 -0
  21. package/skills/ego-browser/learnings/github/notes/overview.md +21 -0
  22. package/skills/ego-browser/learnings/github/tools/open-issues.js +24 -0
  23. package/skills/ego-browser/learnings/github/tools/search-repos.js +30 -0
  24. package/skills/ego-browser/learnings/google/browser-tools/autocomplete.js +11 -0
  25. package/skills/ego-browser/learnings/google/manifest.json +40 -0
  26. package/skills/ego-browser/learnings/google/notes/overview.md +20 -0
  27. package/skills/ego-browser/learnings/google/tools/search-extract.js +25 -0
  28. package/skills/ego-browser/learnings/x-com/browser-tools/extract-post.js +9 -0
  29. package/skills/ego-browser/learnings/x-com/manifest.json +51 -0
  30. package/skills/ego-browser/learnings/x-com/notes/overview.md +18 -0
  31. package/skills/ego-browser/learnings/x-com/notes/timeline.md +16 -0
  32. package/skills/ego-browser/learnings/x-com/tools/search-users.js +18 -0
  33. package/skills/ego-browser/learnings/x-com/tools/timeline.js +21 -0
  34. package/skills/ego-browser/references/clearing-state.md +64 -0
  35. package/skills/ego-browser/references/install-macos.md +59 -0
  36. package/skills/ego-browser/references/install-windows.md +58 -0
  37. package/skills/ego-browser/references/install.md +38 -0
  38. package/skills/ego-browser/scripts/install.sh +234 -0
@@ -0,0 +1,10 @@
1
+ async function(args) {
2
+ const stats = {
3
+ stars: document.querySelector('a[href*="stargazers"]')?.innerText?.replace(/\s/g, '') || '',
4
+ forks: document.querySelector('a[href*="forks"]')?.innerText?.replace(/\s/g, '') || '',
5
+ language: document.querySelector('[itemprop="programmingLanguage"]')?.innerText?.trim() || '',
6
+ name: document.querySelector('[data-target="repo-banner.repoName"]')?.innerText?.trim() || '',
7
+ description: document.querySelector('[data-testid="about-section-notice-p"]')?.nextElementSibling?.innerText?.trim() || '',
8
+ };
9
+ return stats;
10
+ }
@@ -0,0 +1,61 @@
1
+ {
2
+ "id": "github",
3
+ "name": "GitHub",
4
+ "domains": ["github.com", "*.github.com"],
5
+ "notes": ["notes/overview.md"],
6
+ "nodeTools": {
7
+ "search_repos": {
8
+ "description": "Search GitHub repositories and extract results.",
9
+ "path": "tools/search-repos.js",
10
+ "callable": "searchRepos",
11
+ "args": {
12
+ "query": {
13
+ "type": "string",
14
+ "required": true,
15
+ "description": "Search query for repos."
16
+ },
17
+ "maxResults": {
18
+ "type": "integer",
19
+ "required": false,
20
+ "description": "Maximum number of repos to extract."
21
+ }
22
+ },
23
+ "returns": {
24
+ "type": "array",
25
+ "description": "Array of {name, owner, stars, description} objects."
26
+ }
27
+ },
28
+ "get_open_issues": {
29
+ "description": "Extract open issues from a repository.",
30
+ "path": "tools/open-issues.js",
31
+ "callable": "getOpenIssues",
32
+ "args": {
33
+ "owner": {
34
+ "type": "string",
35
+ "required": true,
36
+ "description": "Repository owner (username or org)."
37
+ },
38
+ "repo": {
39
+ "type": "string",
40
+ "required": true,
41
+ "description": "Repository name."
42
+ }
43
+ },
44
+ "returns": {
45
+ "type": "array",
46
+ "description": "Array of {title, number, state} objects."
47
+ }
48
+ }
49
+ },
50
+ "browserTools": {
51
+ "get_repo_stats": {
52
+ "description": "Extract stars, forks, and language from a GitHub repo page.",
53
+ "path": "browser-tools/repo-stats.js",
54
+ "args": {},
55
+ "returns": {
56
+ "type": "object",
57
+ "description": "Object with stars, forks, language."
58
+ }
59
+ }
60
+ }
61
+ }
@@ -0,0 +1,21 @@
1
+ # GitHub Overview
2
+
3
+ ## Page structure
4
+ - Navigation: Header with logo, search, notifications
5
+ - Repository page: `[data-view-component]` wraps the repo body
6
+ - File tree: `#repo-files-toc` or `Box-row`
7
+ - Issues: `[data-test-id="issue-list-item"]` for issue rows
8
+
9
+ ## Navigation patterns
10
+ - Repo URL: `https://github.com/{owner}/{repo}`
11
+ - Search URL: `https://github.com/search?q={query}&type=repositories`
12
+ - Issues URL: `https://github.com/{owner}/{repo}/issues`
13
+ - PR URL: `https://github.com/{owner}/{repo}/pulls`
14
+
15
+ ## Common selectors
16
+ - Repo name: `[data-target="repo-banner.repoName"]`
17
+ - Stars button: `a.Link--muted[data-hydro-click*="stargazers"]`
18
+ - Forks button: `a.Link--muted[data-hydro-click*="forks"]`
19
+ - Language: `[itemprop="programmingLanguage"]`
20
+ - Description: About section content
21
+ - Search input: `input[data-testid="search-input"]` or `input.repository-search-bar-input`
@@ -0,0 +1,24 @@
1
+ const OWNER_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
2
+ const REPO_RE = /^[A-Za-z0-9._-]+$/;
3
+
4
+ export async function getOpenIssues(ctx, args = {}) {
5
+ const owner = args.owner || "";
6
+ const repo = args.repo || "";
7
+ if (!OWNER_RE.test(owner) || !REPO_RE.test(repo)) {
8
+ throw new Error("valid GitHub owner and repo are required");
9
+ }
10
+
11
+ await ctx.openOrReuseTab(`https://github.com/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/issues`, { wait: true });
12
+ await ctx.waitForLoad();
13
+
14
+ const issues = await ctx.js(String.raw`(() => {
15
+ const rows = [...document.querySelectorAll('[data-test-id="issue-list-item"]')]
16
+ return rows.map(el => {
17
+ const title = el.querySelector('a[aria-label*="issue"]')?.innerText?.trim() || el.querySelector('h2 a')?.innerText?.trim() || ''
18
+ const number = el.querySelector('[itemprop="discussionUrl"]')?.innerText?.trim() || ''
19
+ return { title, number }
20
+ }).filter(i => i.title)
21
+ })()`);
22
+
23
+ return issues;
24
+ }
@@ -0,0 +1,30 @@
1
+ function boundedInteger(value, fallback, max) {
2
+ const number = value === undefined ? fallback : Number(value);
3
+ if (!Number.isFinite(number)) return fallback;
4
+ return Math.max(1, Math.min(max, Math.trunc(number)));
5
+ }
6
+
7
+ export async function searchRepos(ctx, args = {}) {
8
+ const query = args.query || "";
9
+ const maxResults = boundedInteger(args.maxResults, 25, 100);
10
+ if (!query) throw new Error("search query is required");
11
+
12
+ await ctx.openOrReuseTab(`https://github.com/search?q=${encodeURIComponent(query)}&type=repositories&s=stars&o=desc`, {
13
+ wait: true,
14
+ });
15
+ await ctx.waitForLoad();
16
+
17
+ const repos = await ctx.js(String.raw`(() => {
18
+ const repos = [...document.querySelectorAll('[data-hydro-click*="repo"]')]
19
+ return repos.slice(0, ${maxResults}).map(el => {
20
+ const name = el.querySelector('h2 a')?.innerText?.trim() || ''
21
+ const link = el.querySelector('h2 a')?.getAttribute('href') || ''
22
+ const parts = link.split('/')
23
+ const owner = parts[1] || ''
24
+ const stars = el.querySelector('a[href*="stargazers"]')?.innerText?.replace(/\s/g, '') || ''
25
+ return { name: parts[2] || name, owner, stars, link }
26
+ }).filter(r => r.name)
27
+ })()`);
28
+
29
+ return repos;
30
+ }
@@ -0,0 +1,11 @@
1
+ async function(args) {
2
+ const suggestionEls = document.querySelectorAll('span.gsqphr');
3
+ if (suggestionEls.length) {
4
+ return [...suggestionEls].map(el => el.innerText?.trim() || '').filter(Boolean);
5
+ }
6
+ const lis = document.querySelectorAll('.ssb-a');
7
+ if (lis.length) {
8
+ return [...lis].map(li => li.innerText?.trim() || '').filter(Boolean);
9
+ }
10
+ return [];
11
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "id": "google",
3
+ "name": "Google Search",
4
+ "domains": ["google.com", "*.google.com", "www.google.com"],
5
+ "notes": ["notes/overview.md"],
6
+ "nodeTools": {
7
+ "search_and_extract": {
8
+ "description": "Perform a Google search and extract top organic results.",
9
+ "path": "tools/search-extract.js",
10
+ "callable": "searchAndExtract",
11
+ "args": {
12
+ "query": {
13
+ "type": "string",
14
+ "required": true,
15
+ "description": "Search query string."
16
+ },
17
+ "maxResults": {
18
+ "type": "integer",
19
+ "required": false,
20
+ "description": "Maximum number of results to extract."
21
+ }
22
+ },
23
+ "returns": {
24
+ "type": "array",
25
+ "description": "Array of {title, url, snippet} objects."
26
+ }
27
+ }
28
+ },
29
+ "browserTools": {
30
+ "get_autocomplete_suggestions": {
31
+ "description": "Get autocomplete suggestions for the current search query.",
32
+ "path": "browser-tools/autocomplete.js",
33
+ "args": {},
34
+ "returns": {
35
+ "type": "array",
36
+ "description": "Array of suggestion strings."
37
+ }
38
+ }
39
+ }
40
+ }
@@ -0,0 +1,20 @@
1
+ # Google Search Overview
2
+
3
+ ## Page structure
4
+ - Search input: `textarea[name="q"]` or `input[name="q"]`
5
+ - Results container: `div#search`
6
+ - Individual result items: `div.g`
7
+ - Result title: `h3` inside `div.g`
8
+ - Result link: `a[href]` inside `div.g a`
9
+ - Result snippet: text content below the title
10
+
11
+ ## Navigation
12
+ - Use `openOrReuseTab` with `https://www.google.com/search?q=...`
13
+ - Results load immediately, no infinite scroll on first page
14
+ - Pagination links at bottom: `a[href*="/search?q="]`
15
+
16
+ ## Common selectors
17
+ - Query box: `textarea[name="q"]`
18
+ - Search button: `input[type="submit"]`
19
+ - Auto-complete dropdown: `div[role="listbox"]`
20
+ - Auto-complete items: `.ssb-a`
@@ -0,0 +1,25 @@
1
+ function boundedInteger(value, fallback, max) {
2
+ const number = value === undefined ? fallback : Number(value);
3
+ if (!Number.isFinite(number)) return fallback;
4
+ return Math.max(1, Math.min(max, Math.trunc(number)));
5
+ }
6
+
7
+ export async function searchAndExtract(ctx, args = {}) {
8
+ const query = args.query || "";
9
+ const maxResults = boundedInteger(args.maxResults, 10, 100);
10
+ if (!query) throw new Error("search query is required");
11
+
12
+ await ctx.openOrReuseTab(`https://www.google.com/search?q=${encodeURIComponent(query)}`, { wait: true });
13
+ await ctx.waitForLoad();
14
+
15
+ const results = await ctx.js(String.raw`(() => {
16
+ const items = [...document.querySelectorAll('div.g')]
17
+ return items.slice(0, ${maxResults}).map(el => ({
18
+ title: el.querySelector('h3')?.innerText?.trim() || '',
19
+ url: el.querySelector('a')?.getAttribute('href') || '',
20
+ snippet: el.querySelector('[data-sncf]')?.innerText?.trim() || el.querySelector('span')?.innerText?.trim() || '',
21
+ })).filter(r => r.title)
22
+ })()`);
23
+
24
+ return results;
25
+ }
@@ -0,0 +1,9 @@
1
+ async function(args) {
2
+ const el = document.querySelector('[data-testid="tweet"]:focus') || document.querySelector('[data-testid="tweet"]');
3
+ if (!el) return { error: 'no active tweet found' };
4
+ return {
5
+ text: el.querySelector('[data-testid="tweetText"]')?.innerText?.trim() || '',
6
+ author: el.querySelector('[data-testid="User-Name"] span')?.innerText?.trim() || '',
7
+ timestamp: el.querySelector('time')?.getAttribute('datetime') || '',
8
+ };
9
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "id": "x-com",
3
+ "name": "X (Twitter)",
4
+ "domains": ["x.com", "*.x.com", "twitter.com", "*.twitter.com"],
5
+ "notes": ["notes/overview.md", "notes/timeline.md"],
6
+ "nodeTools": {
7
+ "get_timeline_posts": {
8
+ "description": "Extract posts from the current X timeline using the anti-click-wrap pattern.",
9
+ "path": "tools/timeline.js",
10
+ "callable": "getTimelinePosts",
11
+ "args": {
12
+ "maxPosts": {
13
+ "type": "integer",
14
+ "required": false,
15
+ "description": "Maximum number of posts to extract."
16
+ }
17
+ },
18
+ "returns": {
19
+ "type": "array",
20
+ "description": "Array of post objects with text, author, and timestamp."
21
+ }
22
+ },
23
+ "search_users": {
24
+ "description": "Search for users on X.",
25
+ "path": "tools/search-users.js",
26
+ "callable": "searchUsers",
27
+ "args": {
28
+ "query": {
29
+ "type": "string",
30
+ "required": true,
31
+ "description": "Search query string."
32
+ }
33
+ },
34
+ "returns": {
35
+ "type": "array",
36
+ "description": "Array of user info objects."
37
+ }
38
+ }
39
+ },
40
+ "browserTools": {
41
+ "post_from_active_element": {
42
+ "description": "Extract tweet data from the currently focused element in the timeline.",
43
+ "path": "browser-tools/extract-post.js",
44
+ "args": {},
45
+ "returns": {
46
+ "type": "object",
47
+ "description": "Tweet text, author, timestamp."
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,18 @@
1
+ # X (Twitter) Overview
2
+
3
+ ## Page structure
4
+ - Main navigation: Left sidebar `[data-testid="AppTabBar"]`
5
+ - Timeline content: `[data-testid="primaryColumn"]`
6
+ - Tweet items: `[data-testid="tweet"]` — each contains text, author, timestamp
7
+ - Compose box: `[data-testid="twaCreateTweetText"]`
8
+
9
+ ## Anti-click-wrap
10
+ - Tweet text is inside `[data-testid="tweetText"]`, click target is `[data-testid="tweet"]`
11
+ - Actions (reply/retweet/like) are inside `div[role="group"]` at the bottom of each tweet card
12
+
13
+ ## Common selectors
14
+ - Tweet text: `[data-testid="tweetText"]`
15
+ - Author name: `[data-testid="User-Name"] span`
16
+ - Timestamp: `time` element with `datetime` attribute
17
+ - Search input: `[data-testid="SearchBox_Search_Input"]`
18
+ - Compose textarea: `[data-testid="twaCreateTweetTextarea"]`
@@ -0,0 +1,16 @@
1
+ # X Timeline Navigation
2
+
3
+ ## Scrolling timeline
4
+ - Timeline is inside `[data-testid="primaryColumn"]`
5
+ - Posts load lazily as you scroll — use `scrollToBottomUntil` to load more
6
+ - Virtual list: DOM only renders visible articles + buffer
7
+
8
+ ## Extracting posts
9
+ - Query `[data-testid="tweet"]` for post elements
10
+ - Each post has `[data-testid="tweetText"]` for content
11
+ - Author info at `[data-testid="User-Name"]`
12
+ - Always extract in one pass via browser-side closure
13
+
14
+ ## Pinned post
15
+ - Pinned post is the first `[data-testid="tweet"]` in timeline
16
+ - To filter pinned post, skip the first element only when at top of timeline
@@ -0,0 +1,18 @@
1
+ export async function searchUsers(ctx, args = {}) {
2
+ const query = args.query || "";
3
+ if (!query) throw new Error("search query is required");
4
+
5
+ await ctx.openOrReuseTab(`https://x.com/search?f=user&q=${encodeURIComponent(query)}`, { wait: true });
6
+ await ctx.waitForLoad();
7
+
8
+ const users = await ctx.js(String.raw`(() => {
9
+ const results = [...document.querySelectorAll('[data-testid="cellInnerDiv"]')]
10
+ return results.map(el => ({
11
+ name: el.querySelector('[data-testid="User-Name"] span')?.innerText?.trim() || '',
12
+ handle: el.querySelector('[data-testid="User-Name"] a')?.innerText?.trim() || '',
13
+ followers: el.querySelector('[data-testid="Follow"]')?.previousElementSibling?.innerText?.trim() || '',
14
+ })).filter(u => u.name || u.handle)
15
+ })()`);
16
+
17
+ return users;
18
+ }
@@ -0,0 +1,21 @@
1
+ function boundedInteger(value, fallback, max) {
2
+ const number = value === undefined ? fallback : Number(value);
3
+ if (!Number.isFinite(number)) return fallback;
4
+ return Math.max(1, Math.min(max, Math.trunc(number)));
5
+ }
6
+
7
+ export async function getTimelinePosts(ctx, args = {}) {
8
+ const maxPosts = boundedInteger(args.maxPosts, 50, 100);
9
+
10
+ const posts = await ctx.js(String.raw`(() => {
11
+ const articles = [...document.querySelectorAll('[data-testid="tweet"]')]
12
+ return articles.slice(0, ${maxPosts}).map(el => ({
13
+ text: el.querySelector('[data-testid="tweetText"]')?.innerText?.trim() || '',
14
+ author: el.querySelector('[data-testid="User-Name"]')?.querySelector('span')?.innerText?.trim() || '',
15
+ handle: el.querySelector('[data-testid="User-Name"]')?.querySelector('a')?.getAttribute('href') || '',
16
+ timestamp: el.querySelector('time')?.getAttribute('datetime') || '',
17
+ }))
18
+ })()`);
19
+
20
+ return posts;
21
+ }
@@ -0,0 +1,64 @@
1
+ # Clearing cookies, cache, and storage
2
+
3
+ Read this file before clearing any cookie, cache, or storage. These commands look interchangeable but differ in how far they reach. A task space picks a browser profile when it is created, and that profile's cookie jar and HTTP cache are shared with the user's own tabs and with every other task space on the same profile. Measured from inside an agent space on a default profile: the current page saw 1 cookie, the profile held 3478 across 1161 domains.
4
+
5
+ ## What each command reaches
6
+
7
+ | Command | Reaches | Getting the state back |
8
+ | ------------------------------------------------- | --------------------- | ---------------------- |
9
+ | `Network.deleteCookies` | one named cookie | sign in to that site |
10
+ | `Storage.clearDataForOrigin` | one origin | sign in to that site |
11
+ | `Page.reload` with `ignoreCache: true` | one Page load | nothing was lost |
12
+ | `Network.clearBrowserCookies` | **the whole profile** | **nothing you can do** |
13
+ | `Network.clearBrowserCache` | **the whole profile** | **nothing you can do** |
14
+ | `Storage.clearCookies` without `browserContextId` | **the whole profile** | **nothing you can do** |
15
+
16
+ The three profile-wide commands take no scope parameter. There is no narrower way to call them, so reach for a different command rather than a different argument.
17
+
18
+ Once a profile-wide clear has run you cannot undo it: the agent has no way to recover the sessions it destroyed, and every site on that profile is signed out. Do not run one intending to restore the state afterwards.
19
+
20
+ ## The entry point does not limit the scope
21
+
22
+ `page.cdp()` selects which target session carries the command. It does not limit what the command touches. `Network.clearBrowserCookies` can only be sent through a Page session — the Network domain does not exist on the browser-level connection — yet it empties the profile's entire cookie jar. A command sent "through this Page" is not a command scoped to this Page.
23
+
24
+ Do not read a rejection from one entry point as permission to use another. If `task.cdp()` refuses a command, sending the same command through `page.cdp()` does not make it safer; it reaches exactly as far.
25
+
26
+ ## Recipes
27
+
28
+ Sign-in wall or a session you want anonymous — clear that one site, not the profile:
29
+
30
+ ```js
31
+ await page.cdp("Storage.clearDataForOrigin", {
32
+ origin: new URL(await page.url()).origin,
33
+ storageTypes: "cookies",
34
+ });
35
+ ```
36
+
37
+ A stale document or a resource that will not reload — reload past the cache instead of emptying it:
38
+
39
+ ```js
40
+ await page.cdp("Page.reload", { ignoreCache: true });
41
+ ```
42
+
43
+ To drop one site's Cache API entries and service workers, use `storageTypes: "cache_storage,service_workers"` with the same `Storage.clearDataForOrigin` call.
44
+
45
+ One specific cookie, such as a stale session or search id:
46
+
47
+ ```js
48
+ const { cookies } = await page.cdp("Network.getAllCookies");
49
+ for (const c of cookies.filter((c) => c.domain.includes("example.com"))) {
50
+ await page.cdp("Network.deleteCookies", {
51
+ name: c.name,
52
+ domain: c.domain,
53
+ path: c.path,
54
+ });
55
+ }
56
+ ```
57
+
58
+ `Network.deleteCookies` removes `httpOnly` cookies, which `document.cookie` cannot. Needing to clear an `httpOnly` cookie is not a reason to widen the scope to the whole profile.
59
+
60
+ ## Profile-wide clears need user approval
61
+
62
+ Treat the three profile-wide commands the way you treat `claimTaskSpace()` and `takeOverTaskSpace()`: only after user approval. Before asking, confirm a scoped command cannot do the job — clearing one site's data is almost always what a single-site problem calls for.
63
+
64
+ When you do need one, tell the user what it costs before running it: how many cookies and domains the profile holds (`Network.getAllCookies`), and which other task spaces share the profile (`listTaskSpaces()` entries carry `profileId` when the runtime reports it). Signing the user out of every site on that profile is not something the agent can put back, and the user may not connect the loss to this task.
@@ -0,0 +1,59 @@
1
+ # Install ego lite on macOS
2
+
3
+ Read `install.md` first; it covers upgrading an older ego lite and the onboarding step shared by every platform. This file has the macOS install steps, the commands that confirm the installation, and macOS troubleshooting.
4
+
5
+ ## Run the install script
6
+
7
+ The install script lives at `scripts/install.sh` in this skill and supports macOS only (`uname -s` is `Darwin`). It will:
8
+
9
+ - Download the ego lite installer (a DMG) for your CPU architecture (arm64 / x64).
10
+ - Install `ego lite.app` to `/Applications` (falling back to `~/Applications` when needed).
11
+ - Strip the quarantine attribute to keep Gatekeeper from blocking the first launch.
12
+ - After installing, launch the `ego lite` app.
13
+
14
+ Run the script (use the script's actual path under this skill's directory):
15
+
16
+ ```bash
17
+ sh skills/ego-browser/scripts/install.sh
18
+ ```
19
+
20
+ After installing, the script opens the ego lite app directly. If ego lite is already installed, the script skips the download and opens the app directly.
21
+
22
+ After the script opens the ego lite app, the user completes the first-run onboarding in the app (see `install.md`). Onboarding registers the `ego-browser` command on the PATH, usually under `~/.local/bin`.
23
+
24
+ ## After installing: confirm `ego-browser` is available
25
+
26
+ Once the user has finished onboarding, confirm the command is ready:
27
+
28
+ ```bash
29
+ command -v ego-browser
30
+ ```
31
+
32
+ If it reports that the command isn't found, `~/.local/bin` is most likely not on the current PATH. Fix it temporarily and retry:
33
+
34
+ ```bash
35
+ export PATH="$HOME/.local/bin:$PATH"
36
+ command -v ego-browser
37
+ ```
38
+
39
+ Once the command exists, verify the runtime:
40
+
41
+ ```bash
42
+ ego-browser nodejs <<'EOF'
43
+ console.log('ego-browser ready')
44
+ EOF
45
+ ```
46
+
47
+ Printing `ego-browser ready` means the environment is ready. Finally confirm that the installed ego lite prints its usage guide:
48
+
49
+ ```bash
50
+ ego-browser skill
51
+ ```
52
+
53
+ Then return to the user's original task, as `install.md` describes.
54
+
55
+ ## Troubleshooting
56
+
57
+ - **Download failed**: the script retries 3 times automatically; if it still fails, it's usually a network issue — have the user check their network and retry.
58
+ - **Gatekeeper still blocks it**: the script already tries to strip quarantine; if the first launch is still blocked, have the user allow ego lite manually under System Settings → Privacy & Security.
59
+ - **Command still unavailable after onboarding**: confirm `~/.local/bin` is on the PATH (see above). Otherwise have the user reopen ego lite, finish onboarding, and retry.
@@ -0,0 +1,58 @@
1
+ # Install ego lite on Windows
2
+
3
+ Read `install.md` first; it covers upgrading an older ego lite and the onboarding step shared by every platform. This file has the Windows install steps, the commands that confirm the installation, and Windows troubleshooting.
4
+
5
+ ## Run the installer
6
+
7
+ There is no install script for Windows. Have the user download the installer from https://lite.ego.app/ and run it, then open ego lite.
8
+
9
+ After the app opens, the user completes the first-run onboarding in the app (see `install.md`). Onboarding registers the `ego-browser` command on the PATH. The PATH entry only reaches processes started afterwards, so open a new terminal before checking for the command.
10
+
11
+ ## After installing: confirm `ego-browser` is available
12
+
13
+ Once the user has finished onboarding, open a new terminal and confirm the command is ready. The command depends on the shell.
14
+
15
+ PowerShell:
16
+
17
+ ```powershell
18
+ Get-Command ego-browser
19
+ ```
20
+
21
+ cmd:
22
+
23
+ ```bat
24
+ where ego-browser
25
+ ```
26
+
27
+ Git Bash:
28
+
29
+ ```bash
30
+ command -v ego-browser
31
+ ```
32
+
33
+ Once the command exists, verify the runtime. In PowerShell or cmd, where heredoc does not exist:
34
+
35
+ ```powershell
36
+ ego-browser nodejs -e "console.log('ego-browser ready')"
37
+ ```
38
+
39
+ In Git Bash:
40
+
41
+ ```bash
42
+ ego-browser nodejs <<'EOF'
43
+ console.log('ego-browser ready')
44
+ EOF
45
+ ```
46
+
47
+ Printing `ego-browser ready` means the environment is ready. Finally confirm that the installed ego lite prints its usage guide:
48
+
49
+ ```powershell
50
+ ego-browser skill
51
+ ```
52
+
53
+ Then return to the user's original task, as `install.md` describes.
54
+
55
+ ## Troubleshooting
56
+
57
+ - **SmartScreen blocks the installer**: have the user choose "More info" and then "Run anyway" in the SmartScreen dialog.
58
+ - **Command still unavailable after onboarding**: open a new terminal first, because an existing one keeps the PATH it started with. Otherwise have the user reopen ego lite, finish onboarding, and retry.
@@ -0,0 +1,38 @@
1
+ # Install or upgrade ego lite
2
+
3
+ Read this file only when `ego-browser` is not found, when `ego-browser skill` reports an unknown command, or when the user asks to install ego lite. For day-to-day browser work, run `ego-browser skill` and follow its output.
4
+
5
+ The ego-browser skill depends on the ego lite browser: the `ego-browser` command is provided by the ego lite app. Once ego lite is installed and you've completed onboarding, no additional setup is normally needed.
6
+
7
+ ego lite website: https://lite.ego.app/
8
+
9
+ ## Upgrade an older ego lite
10
+
11
+ If `ego-browser` exists but `ego-browser skill` reports `Unknown command: skill`, the installed ego lite is older than this skill. Ask the user for approval, then run:
12
+
13
+ ```bash
14
+ ego-browser upgrade
15
+ ```
16
+
17
+ If `upgrade` is also an unknown command, reinstall with the steps for the user's operating system. Then continue with "After installing: confirm `ego-browser` is available" in that file.
18
+
19
+ ## Install
20
+
21
+ The install steps, the commands that confirm the installation, and the troubleshooting differ by operating system. Read the file for the machine you are running on and follow it to the end:
22
+
23
+ - macOS: [install-macos.md](install-macos.md)
24
+ - Windows: [install-windows.md](install-windows.md)
25
+ - Other platforms: there is no installer. Have the user check https://lite.ego.app/ for a build; if none exists, ego-browser is not available on this machine.
26
+
27
+ ## Onboarding
28
+
29
+ Onboarding is a step the user completes in the GUI, on every platform:
30
+
31
+ - Choose to import data from Chrome or another browser as needed.
32
+ - Onboarding registers the `ego-browser` command on the PATH.
33
+
34
+ After the app opens, wait for the user to confirm they've finished onboarding before continuing. The per-platform file explains how to confirm the command is available afterwards.
35
+
36
+ ## After that, return to the original task
37
+
38
+ Once the environment is ready, return to the user's original task: run `ego-browser skill` and follow its output.