symbiot 0.60.0 → 0.61.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +9 -0
- package/browser.html +94 -0
- package/headless.mjs +64 -8
- package/index.mjs +12 -1
- package/links.mjs +2 -2
- package/package.json +4 -3
- package/server.mjs +7 -2
- package/ui.mjs +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
|
|
4
4
|
|
|
5
|
+
## 0.61.0
|
|
6
|
+
|
|
7
|
+
- **The Symbiot Browser.** Where you sign in to your sites for Symbiot, now a window of its own, named and marked as Symbiot's, instead of a plain Chrome window that looked like any other. It opens on a page of your sites (Gmail, Outlook, calendars, GitHub, Slack, WhatsApp, Jira, LinkedIn…), each saying whether you're signed in, with a **Sign in** (or **Connect**) button, an address bar for any other site, and **Done**, which closes it and has Symbiot check each site. Open it from **Settings → Links → Open the Symbiot Browser**, or `symbiot browser [site]`; connecting a site anywhere in Symbiot opens it too.
|
|
8
|
+
- **No more invisible sign-in windows.** If Symbiot's hidden browser was running, a sign-in window could open inside it, where you'd never see it. It's closed first now.
|
|
9
|
+
- **No more stray empty windows.** While the Symbiot Browser (or any sign-in window) is open, Symbiot's background reads wait for Done instead of starting a second Chrome, which popped an empty window into yours.
|
|
10
|
+
|
|
5
11
|
## 0.60.0
|
|
6
12
|
|
|
7
13
|
- **A release no longer trips on file times.** Linux stamps files with a clock a few milliseconds behind the precise one, so a run that asked a question straight away could look as if it asked before it started, and wasn't noted as waiting on you. Symbiot now allows for that (it also made a test fail on GitHub's machines, which held up 0.59.0).
|
package/README.md
CHANGED
|
@@ -1419,6 +1419,15 @@ expects. Symbiot rescales for `xdotool` and for `cliclick` on a Retina screen, b
|
|
|
1419
1419
|
it can't read the scale on Wayland, and ydotool's moves follow your pointer
|
|
1420
1420
|
acceleration, so a click there can land off target.
|
|
1421
1421
|
|
|
1422
|
+
## The Symbiot Browser
|
|
1423
|
+
|
|
1424
|
+
Symbiot reads your sites (Gmail, LinkedIn, WhatsApp, Jira…) with your own sign-in, in
|
|
1425
|
+
its own browser. **Settings → Links → Open the Symbiot Browser** (or `symbiot browser`)
|
|
1426
|
+
opens it: a window of its own listing your sites, each with whether you're signed in and
|
|
1427
|
+
a **Sign in** button, plus an address bar for any other site. Sign in the usual way
|
|
1428
|
+
(the site's own login and 2FA; Symbiot never sees a password), then click **Done**: it
|
|
1429
|
+
closes and Symbiot checks each site.
|
|
1430
|
+
|
|
1422
1431
|
## Any language
|
|
1423
1432
|
|
|
1424
1433
|
Symbiot uses the language your computer is set to, or any language you name in
|
package/browser.html
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
6
|
+
<title>Symbiot Browser</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
|
+
<meta name="theme-color" content="#0B0C0E">
|
|
9
|
+
<style>
|
|
10
|
+
/* The Symbiot Browser's own page: where you sign in to your sites, once, for Symbiot.
|
|
11
|
+
It runs in Symbiot's browser profile, so a sign-in here is the one Symbiot uses. */
|
|
12
|
+
@font-face{font-family:"Geist";src:url("/fonts/Geist-Variable.woff2") format("woff2");font-weight:100 900;font-display:swap}
|
|
13
|
+
:root{--bg:#0B0C0E;--panel:#131518;--line:#2A2E34;--bone:#F3F0EA;--text:#C9CDD3;--faint:#8A919B;--amber:#F2A541;--ok:#4FBE8C;--bad:#FF8A75;color-scheme:dark;
|
|
14
|
+
--sans:"Geist",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
|
|
15
|
+
@media (prefers-color-scheme: light){:root{--bg:#F4F2EE;--panel:#FFFFFF;--line:#DEDAD2;--bone:#11151B;--text:#3A414C;--faint:#6B7380;--amber:#B86A0C;--ok:#0B7A55;--bad:#B4361F;color-scheme:light}}
|
|
16
|
+
*{box-sizing:border-box}
|
|
17
|
+
body{margin:0;background:var(--bg);color:var(--text);font:15px/1.5 var(--sans)}
|
|
18
|
+
.wrap{max-width:880px;margin:0 auto;padding:28px 24px 40px;display:grid;gap:22px}
|
|
19
|
+
header{display:flex;align-items:center;gap:14px}
|
|
20
|
+
header img{width:44px;height:44px;border-radius:12px}
|
|
21
|
+
h1{margin:0;font:650 24px/1.2 var(--sans);letter-spacing:-.02em;color:var(--bone)}
|
|
22
|
+
.lede{margin:2px 0 0;color:var(--faint);font-size:14px;max-width:62ch}
|
|
23
|
+
form.go{display:flex;gap:10px}
|
|
24
|
+
form.go input{flex:1;min-width:0;padding:12px 16px;border-radius:999px;border:1px solid var(--line);background:var(--panel);color:var(--bone);font:15px var(--sans)}
|
|
25
|
+
button{font:600 14px var(--sans);border-radius:999px;padding:10px 18px;border:1px solid var(--line);background:transparent;color:var(--bone);cursor:pointer}
|
|
26
|
+
button.main{background:var(--bone);color:var(--bg);border-color:var(--bone)}
|
|
27
|
+
button:focus-visible,input:focus-visible{outline:2px solid var(--amber);outline-offset:2px}
|
|
28
|
+
h2{margin:6px 0 0;font:600 12px var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
|
|
29
|
+
.sites{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}
|
|
30
|
+
.site{display:flex;align-items:center;gap:12px;padding:14px 16px;border:1px solid var(--line);border-radius:16px;background:var(--panel)}
|
|
31
|
+
.site .n{flex:1;min-width:0}
|
|
32
|
+
.site b{display:block;color:var(--bone);font-weight:600}
|
|
33
|
+
.site small{display:block;color:var(--faint);font-size:12.5px}
|
|
34
|
+
.site small.ok{color:var(--ok)}.site small.bad{color:var(--bad)}.site small.wait{color:var(--amber)}
|
|
35
|
+
.site button{padding:8px 14px;font-size:13px;flex:none}
|
|
36
|
+
.done{display:flex;align-items:center;gap:16px;flex-wrap:wrap;padding:18px 20px;border-radius:18px;border:1px solid var(--line);background:var(--panel)}
|
|
37
|
+
.done p{margin:0;flex:1;min-width:220px;font-size:14px}
|
|
38
|
+
.msg{min-height:20px;font-size:14px;color:var(--amber)}
|
|
39
|
+
</style>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<main class="wrap">
|
|
43
|
+
<header>
|
|
44
|
+
<img src="/favicon.svg" alt="">
|
|
45
|
+
<div><h1>Symbiot Browser</h1>
|
|
46
|
+
<p class="lede">Symbiot’s own browser. Sign in to your sites here, once, the usual way (their own login, 2FA and all). Symbiot then reads them for you with that sign-in. It never sees your passwords.</p></div>
|
|
47
|
+
</header>
|
|
48
|
+
|
|
49
|
+
<form class="go" id="go"><input id="addr" placeholder="Go to any site: a name like gmail, or an address" autocomplete="off" aria-label="Site to open"><button class="main" type="submit">Open</button></form>
|
|
50
|
+
<div class="msg" id="msg" role="status"></div>
|
|
51
|
+
|
|
52
|
+
<section id="list"></section>
|
|
53
|
+
|
|
54
|
+
<div class="done">
|
|
55
|
+
<p><b>Signed in?</b> Click Done: the Symbiot Browser closes and Symbiot checks each site, so the ticks come up in Symbiot within a minute.</p>
|
|
56
|
+
<button class="main" id="done" type="button">Done</button>
|
|
57
|
+
</div>
|
|
58
|
+
</main>
|
|
59
|
+
<script>
|
|
60
|
+
var T = new URLSearchParams(location.search).get("t") || "";
|
|
61
|
+
function api(path, body) { return fetch(path, { method: body ? "POST" : "GET", headers: { "x-symbiot-token": T, "content-type": "application/json" }, body: body ? JSON.stringify(body) : undefined }).then(function (r) { return r.json(); }); }
|
|
62
|
+
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); }
|
|
63
|
+
function say(t) { document.getElementById("msg").textContent = t || ""; }
|
|
64
|
+
// a window of this same browser (so the sign-in is Symbiot's), the size of a laptop screen
|
|
65
|
+
function openSite(url) { var w = window.open(url, "_blank", "popup=no,width=1200,height=860"); if (!w) say("Your browser blocked the window: allow pop-ups for this page, then try again."); return w; }
|
|
66
|
+
function fromInput(v) { v = String(v || "").trim(); if (!v) return ""; if (/^https?:\/\//i.test(v)) return v; if (/^[a-z0-9-]+$/i.test(v)) return "https://" + v.toLowerCase() + ".com"; return "https://" + v; }
|
|
67
|
+
var STATE = { off: ["Not connected", ""], signin: ["Not signed in yet", "wait"], signedout: ["Signed out", "bad"], error: ["Needs attention", "bad"], ok: ["Signed in", "ok"] };
|
|
68
|
+
function render(d) {
|
|
69
|
+
var items = (d && d.items) || [], groups = (d && d.groups) || [], h = "";
|
|
70
|
+
groups.forEach(function (g) {
|
|
71
|
+
var its = items.filter(function (x) { return x.group === g; }); if (!its.length) return;
|
|
72
|
+
h += "<h2>" + esc(g) + "</h2><div class='sites'>" + its.map(function (x) {
|
|
73
|
+
var s = STATE[x.state] || STATE.off, label = x.state === "off" ? "Connect" : x.state === "ok" ? "Open" : "Sign in";
|
|
74
|
+
return "<div class='site'><div class='n'><b>" + esc(x.name) + "</b><small class='" + s[1] + "'>" + (x.state === "ok" ? "✓ " : "") + esc(s[0]) + "</small></div><button type='button' data-id='" + esc(x.id) + "' data-url='" + esc(x.url) + "' data-state='" + esc(x.state) + "'>" + label + "</button></div>";
|
|
75
|
+
}).join("") + "</div>";
|
|
76
|
+
});
|
|
77
|
+
document.getElementById("list").innerHTML = h;
|
|
78
|
+
}
|
|
79
|
+
function load() { api("/api/links").then(render).catch(function () { say("Symbiot isn't answering. Is it still running?"); }); }
|
|
80
|
+
document.getElementById("list").addEventListener("click", function (e) {
|
|
81
|
+
var b = e.target.closest && e.target.closest("button[data-id]"); if (!b) return;
|
|
82
|
+
var url = b.getAttribute("data-url"), id = b.getAttribute("data-id");
|
|
83
|
+
var w = openSite(url); // first, while the click still counts (pop-up rules)
|
|
84
|
+
if (b.getAttribute("data-state") === "off") api("/api/links/link", { id: id, here: true }).then(function (r) {
|
|
85
|
+
if (r && r.error) { if (w) w.close(); say(r.error); } else { say("Sign in to it in the window that opened, then come back here."); load(); }
|
|
86
|
+
}).catch(function () {});
|
|
87
|
+
else say("Sign in to it in the window that opened, then come back here.");
|
|
88
|
+
});
|
|
89
|
+
document.getElementById("go").addEventListener("submit", function (e) { e.preventDefault(); var u = fromInput(document.getElementById("addr").value); if (u) openSite(u); });
|
|
90
|
+
document.getElementById("done").addEventListener("click", function () { say("Closing, and checking your sites…"); api("/api/browser/done", {}).catch(function () {}); });
|
|
91
|
+
load(); setInterval(load, 5000);
|
|
92
|
+
</script>
|
|
93
|
+
</body>
|
|
94
|
+
</html>
|
package/headless.mjs
CHANGED
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
// The browser keeps its own profile in ~/.config/symbiot/browser: sign in to a
|
|
20
20
|
// site once there (signIn opens it as a normal window) and later maps see it
|
|
21
21
|
// signed in. It's separate from your everyday browser profile.
|
|
22
|
-
import { spawn } from "node:child_process";
|
|
22
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
23
23
|
import { join, resolve as resolvePath } from "node:path";
|
|
24
|
-
import { mkdirSync, existsSync, statSync } from "node:fs";
|
|
24
|
+
import { mkdirSync, existsSync, statSync, readlinkSync } from "node:fs";
|
|
25
25
|
import { CONFIG_DIR, chromeBinary, loadConfig, saveConfig } from "./core.mjs";
|
|
26
26
|
import { loadScreens, addPageScreen, center, stitchPng, stitchListPng } from "./screens.mjs";
|
|
27
27
|
|
|
@@ -98,6 +98,18 @@ const browserOpen = () => !!(live && !live.c.closed());
|
|
|
98
98
|
// Don't leave it running when Symbiot exits (a restart, Ctrl+C).
|
|
99
99
|
process.once("exit", () => { if (live && live.proc.exitCode === null) try { live.proc.kill("SIGKILL"); } catch {} });
|
|
100
100
|
|
|
101
|
+
// Is a visible Chrome (the Symbiot Browser) using Symbiot's profile? Its SingletonLock names the process
|
|
102
|
+
// ("host-1234", Linux and Mac); a lock whose process has gone is a leftover.
|
|
103
|
+
const OWN = new Set(); // every hidden Chrome this Symbiot started
|
|
104
|
+
function windowOpen() {
|
|
105
|
+
try {
|
|
106
|
+
const m = /-(\d+)$/.exec(readlinkSync(join(PROFILE, "SingletonLock")));
|
|
107
|
+
if (!m) return false;
|
|
108
|
+
if (OWN.has(Number(m[1]))) return false; // our own hidden browser (running, or still closing) isn't a window
|
|
109
|
+
process.kill(Number(m[1]), 0);
|
|
110
|
+
return true;
|
|
111
|
+
} catch { return false; }
|
|
112
|
+
}
|
|
101
113
|
// Start the hidden browser and open a tab: { proc, c, page: { send, until, idle } }.
|
|
102
114
|
async function launch() {
|
|
103
115
|
const chrome = chromeBinary();
|
|
@@ -105,7 +117,11 @@ async function launch() {
|
|
|
105
117
|
? "Mapping a page needs a desktop browser (Chrome, Chromium, Edge or Brave), which a phone doesn't have. Map pages from Symbiot on your computer."
|
|
106
118
|
: "Mapping a page needs Chrome, Chromium, Edge or Brave, and none was found.");
|
|
107
119
|
mkdirSync(PROFILE, { recursive: true, mode: 0o700 });
|
|
120
|
+
// the Symbiot Browser (or a sign-in window) has the profile: a second Chrome would hand
|
|
121
|
+
// over to it and pop an empty window into it, so wait for Done instead
|
|
122
|
+
if (windowOpen()) throw new Error("The Symbiot Browser is open: Symbiot reads your sites once you click Done in it.");
|
|
108
123
|
const proc = spawn(chrome, browserArgs(true), { stdio: ["ignore", "ignore", "pipe", "pipe", "pipe"] });
|
|
124
|
+
if (proc.pid) OWN.add(proc.pid);
|
|
109
125
|
let err = ""; proc.stderr.on("data", (d) => { err = (err + d).slice(-2000); });
|
|
110
126
|
const failed = new Promise((resolve, reject) => { proc.on("error", reject); }); failed.catch(() => {});
|
|
111
127
|
const c = connect(proc), b = { proc, c, shown: "", timer: null };
|
|
@@ -801,13 +817,53 @@ function uploadRegion(id, regionId, files, { confirmed = false } = {}) {
|
|
|
801
817
|
// accept cookies) once. Close it before mapping: the profile is shared. The hidden
|
|
802
818
|
// browser is closed first, or the window would open in it, out of sight.
|
|
803
819
|
async function signIn(input) {
|
|
804
|
-
const url = siteUrl(input)
|
|
820
|
+
const url = siteUrl(input);
|
|
805
821
|
if (!url) return { error: "Give the site to sign in to: a name (gmail), a host or a web address." };
|
|
806
|
-
|
|
822
|
+
return openSymbiotBrowser(url);
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// ---- the Symbiot Browser --------------------------------------------------------
|
|
826
|
+
// Where you sign in to your sites for Symbiot: a window of its own, named and marked
|
|
827
|
+
// as Symbiot's, on Symbiot's browser profile (so its sign-ins are the ones the hidden
|
|
828
|
+
// browser reads with). It opens on its own page (browser.html, served by the app:
|
|
829
|
+
// your sites, signed in or not, a Sign in for each, an address bar, Done), plus the
|
|
830
|
+
// site asked for in a window beside it. Chrome lets one copy use a profile at a time,
|
|
831
|
+
// and a window asked of a hidden copy opens hidden: so any hidden copy on this profile
|
|
832
|
+
// is closed first, Symbiot's or a stray one (Linux, Mac).
|
|
833
|
+
let HUB = ""; // the app's address for that page: http://127.0.0.1:<port>/browser?t=<token>
|
|
834
|
+
function setBrowserHub(url) { HUB = String(url || ""); }
|
|
835
|
+
function profilePids({ headless } = {}) {
|
|
836
|
+
if (process.platform === "win32") return [];
|
|
837
|
+
try {
|
|
838
|
+
const out = spawnSync("ps", ["-eo", "pid=,args="], { encoding: "utf8", timeout: 5000 }).stdout || "";
|
|
839
|
+
return out.split("\n").map((l) => l.trim().match(/^(\d+)\s+(.*)$/)).filter(Boolean)
|
|
840
|
+
.filter(([, , a]) => a.includes("--user-data-dir=" + PROFILE) && !/--type=/.test(a) && (headless === undefined || /--headless/.test(a) === headless))
|
|
841
|
+
.map(([, pid]) => Number(pid)).filter((p) => p && p !== process.pid);
|
|
842
|
+
} catch { return []; }
|
|
843
|
+
}
|
|
844
|
+
const kill = (pids) => { for (const p of pids) try { process.kill(p, "SIGTERM"); } catch {} };
|
|
845
|
+
async function openSymbiotBrowser(url = "") {
|
|
846
|
+
const chrome = chromeBinary();
|
|
847
|
+
if (!chrome) return { error: "The Symbiot Browser needs Chrome, Chromium, Edge or Brave on this computer, and none was found." };
|
|
807
848
|
await oneAtATime(closeBrowser);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
849
|
+
const hidden = profilePids({ headless: true }); if (hidden.length) { kill(hidden); await sleep(800); }
|
|
850
|
+
try {
|
|
851
|
+
mkdirSync(PROFILE, { recursive: true, mode: 0o700 });
|
|
852
|
+
const base = ["--user-data-dir=" + PROFILE, "--no-first-run", "--no-default-browser-check",
|
|
853
|
+
...(process.platform === "linux" ? ["--password-store=basic", "--class=SymbiotBrowser"] : process.platform === "darwin" ? ["--use-mock-keychain"] : [])];
|
|
854
|
+
const page = HUB ? ["--app=" + HUB, "--window-size=1040,820"] : [];
|
|
855
|
+
if (page.length) spawn(chrome, [...base, ...page], { detached: true, stdio: "ignore" }).unref();
|
|
856
|
+
if (url) setTimeout(() => { try { spawn(chrome, [...base, "--new-window", url], { detached: true, stdio: "ignore" }).unref(); } catch {} }, page.length ? 1500 : 0);
|
|
857
|
+
if (!page.length && !url) spawn(chrome, [...base, "--new-window", "about:blank"], { detached: true, stdio: "ignore" }).unref();
|
|
858
|
+
} catch (e) { return { error: String((e && e.message) || e) }; }
|
|
859
|
+
return { ok: true, ...(url ? { url } : {}), browser: true };
|
|
860
|
+
}
|
|
861
|
+
// Done: the Symbiot Browser closes (its sign-ins are saved), so the hidden browser can read with them.
|
|
862
|
+
async function closeSymbiotBrowser() {
|
|
863
|
+
const open = profilePids({ headless: false });
|
|
864
|
+
kill(open);
|
|
865
|
+
for (let i = 0; i < 20 && profilePids({ headless: false }).length; i++) await sleep(250);
|
|
866
|
+
return { closed: open.length };
|
|
811
867
|
}
|
|
812
868
|
|
|
813
|
-
export { siteUrl, browserArgs, mapPage, wholePage, readPage, readTexts, readLinkedIn, LI_READ, pagePicture, pageClip, CLIP, isSend, pressRegion, typeRegion, uploadRegion, uploadFiles, attachFiles, FILE_BOX, scrollPage, SCROLLS, signIn, keepBrowserOpen, closeBrowser, browserOpen, trustedSites, isTrusted, trustSite, untrustSite, PROFILE };
|
|
869
|
+
export { siteUrl, browserArgs, mapPage, wholePage, readPage, readTexts, readLinkedIn, LI_READ, pagePicture, pageClip, CLIP, isSend, pressRegion, typeRegion, uploadRegion, uploadFiles, attachFiles, FILE_BOX, scrollPage, SCROLLS, signIn, keepBrowserOpen, closeBrowser, browserOpen, trustedSites, isTrusted, trustSite, untrustSite, PROFILE , openSymbiotBrowser, closeSymbiotBrowser, setBrowserHub };
|
package/index.mjs
CHANGED
|
@@ -351,6 +351,15 @@ async function cmdAway() {
|
|
|
351
351
|
if (!r) { console.log(c.y("Symbiot's app isn't running.") + c.d(" Start it with symbiot app, then Away works.")); process.exitCode = 1; return; }
|
|
352
352
|
if (r.error) { console.log(c.y(r.error)); process.exitCode = 1; }
|
|
353
353
|
}
|
|
354
|
+
// symbiot browser [site]: the Symbiot Browser, where you sign in to your sites for
|
|
355
|
+
// Symbiot (headless.mjs openSymbiotBrowser), opened by the running app.
|
|
356
|
+
async function cmdBrowser() {
|
|
357
|
+
const site = argv.slice(1).find((a) => !a.startsWith("-")) || "";
|
|
358
|
+
const r = await viaApp("/api/browser/open", site ? { url: site } : {});
|
|
359
|
+
if (!r) { console.log(c.y("Symbiot's app isn't running.") + c.d(" Open Symbiot first, then the Symbiot Browser opens from Settings → Links, or with symbiot browser.")); process.exitCode = 1; return; }
|
|
360
|
+
if (r.error) { console.log(c.y(r.error)); process.exitCode = 1; return; }
|
|
361
|
+
console.log(`${c.g("✓")} Opened the Symbiot Browser${site ? " at " + (r.url || site) : ""}. Sign in to your sites there, then click Done.`);
|
|
362
|
+
}
|
|
354
363
|
// symbiot open: what the app-menu icon runs. Running: its window comes up. Not
|
|
355
364
|
// running: it starts in the background (no terminal; what it prints goes to
|
|
356
365
|
// ~/.config/symbiot/app.log) and opens its own window.
|
|
@@ -446,7 +455,8 @@ async function cmdScreens() {
|
|
|
446
455
|
symbiot screens whole <id> map all of that page in one tall screenshot
|
|
447
456
|
(where a list scrolls inside the page, like
|
|
448
457
|
Gmail's mail, that list opened out)
|
|
449
|
-
symbiot
|
|
458
|
+
symbiot browser [site] the Symbiot Browser: sign in to your sites for Symbiot
|
|
459
|
+
symbiot screens signin <site> sign in once, in the Symbiot Browser
|
|
450
460
|
--yes is needed (press, type, upload) unless the page's site is under Trusted sites in the app's Settings.
|
|
451
461
|
While the app runs, these use its hidden browser, which stays open a few minutes:
|
|
452
462
|
press on the screen the last command printed carries on from that page as it is
|
|
@@ -926,6 +936,7 @@ async function main() {
|
|
|
926
936
|
// an old copy npm replaced), and Claude Code won't run from a deleted folder
|
|
927
937
|
if (cmd === "app" || cmd === "ui") { try { process.chdir(homedir()); } catch {} return cmdApp(); }
|
|
928
938
|
if (cmd === "away") return cmdAway();
|
|
939
|
+
if (cmd === "browser") return cmdBrowser();
|
|
929
940
|
if (cmd === "open") return cmdOpen();
|
|
930
941
|
if (cmd === "uninstall") return cmdUninstall();
|
|
931
942
|
if (cmd === "models" || cmd === "hardware") return cmdModels();
|
package/links.mjs
CHANGED
|
@@ -89,7 +89,7 @@ function stateOf(e, l, watches) {
|
|
|
89
89
|
// finished signing in in the window is still "sign in"
|
|
90
90
|
if (w.error && /^Signed out/.test(w.error)) return w.checked ? { state: "signedout", note: "Signed out: click it to sign in again." } : { state: "signin" };
|
|
91
91
|
if (w.checked) return { state: "ok", checked: w.checked, ...(w.error ? { note: w.error } : {}) };
|
|
92
|
-
return { state: "signin", ...(w.error && !/in use|busy|already running/i.test(w.error) ? { note: w.error } : {}) };
|
|
92
|
+
return { state: "signin", ...(w.error && !/in use|busy|already running|Symbiot Browser is open/i.test(w.error) ? { note: w.error } : {}) };
|
|
93
93
|
}
|
|
94
94
|
function linksState() {
|
|
95
95
|
const c = catalog(), l = linked(), watches = watchState().watches;
|
|
@@ -98,7 +98,7 @@ function linksState() {
|
|
|
98
98
|
return { items, groups, linked: items.filter((x) => x.state !== "off").length, ...(c.file ? { file: c.file } : {}), ...(c.error ? { error: c.error } : {}) };
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
-
// Link one: trust its hosts, watch its page, open it to sign in. Clicking a link
|
|
101
|
+
// Link one: trust its hosts, watch its page, open it to sign in (in the Symbiot Browser). Clicking a link
|
|
102
102
|
// that's already there just opens it again to sign in (signed out, or a second
|
|
103
103
|
// account). Gives { ok, url, item } or { error }.
|
|
104
104
|
// what counts as an inbox or chat for Free's one
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "symbiot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice \u2014 Claude, OpenAI, Gemini, or a local model.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"claudesetup.mjs",
|
|
55
55
|
"voice.mjs",
|
|
56
56
|
"licence.mjs",
|
|
57
|
-
"lang.mjs"
|
|
57
|
+
"lang.mjs",
|
|
58
|
+
"browser.html"
|
|
58
59
|
],
|
|
59
60
|
"engines": {
|
|
60
61
|
"node": ">=18"
|
|
@@ -87,7 +88,7 @@
|
|
|
87
88
|
"@anthropic-ai/sdk": "^0.128.0"
|
|
88
89
|
},
|
|
89
90
|
"scripts": {
|
|
90
|
-
"test": "node test/load.mjs && node test/fixtures.mjs && node test/links.mjs && node test/post.mjs && node test/peek.mjs && node test/mind.mjs && node test/knowledge.mjs && node test/checks.mjs &&node test/lanes.mjs && node test/handback.mjs && node test/autoallow.mjs && node test/guard.mjs && node test/reports.mjs && node test/sandbox.mjs && node test/adapt.mjs && node test/urgent.mjs && node test/drafts.mjs && node test/linkedin.mjs && node test/secrets.mjs && node test/work.mjs && node test/mapknn.mjs && node test/away.mjs && node test/voice.mjs && node test/licence.mjs && node test/lang.mjs && node test/subscription.mjs && node test/claudesetup.mjs && node test/launcher.mjs && node test/onboarding.mjs && node test/windows.mjs && node test/marketing.mjs && node test/tray.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
91
|
+
"test": "node test/load.mjs && node test/fixtures.mjs && node test/links.mjs && node test/post.mjs && node test/peek.mjs && node test/mind.mjs && node test/knowledge.mjs && node test/checks.mjs &&node test/lanes.mjs && node test/handback.mjs && node test/autoallow.mjs && node test/guard.mjs && node test/reports.mjs && node test/sandbox.mjs && node test/adapt.mjs && node test/urgent.mjs && node test/drafts.mjs && node test/linkedin.mjs && node test/secrets.mjs && node test/work.mjs && node test/mapknn.mjs && node test/away.mjs && node test/voice.mjs && node test/licence.mjs && node test/lang.mjs && node test/browser.mjs && node test/subscription.mjs && node test/claudesetup.mjs && node test/launcher.mjs && node test/onboarding.mjs && node test/windows.mjs && node test/marketing.mjs && node test/tray.mjs && node test/paths.mjs && node test/smoke.mjs && node test/app.mjs && node test/install.mjs",
|
|
91
92
|
"lint": "eslint .",
|
|
92
93
|
"postinstall": "node postinstall.mjs"
|
|
93
94
|
},
|
package/server.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { computeDrift } from "./drift.mjs";
|
|
|
25
25
|
import { addTask, toggleTask, removeTask, restoreTask, syncTasks, taskType, pushTasks, pendingReview, workingDiff, learnNpm, withReleases, releaseInput, approveRepo, approveChanges, sendBack, setAutoMerge } from "./tasks.mjs";
|
|
26
26
|
import { repoReview, repoSuggest, folderSuggest, taskChat, clearTaskChat, mailState, setMail, sentMail, produce, releaseNotes } from "./writeups.mjs";
|
|
27
27
|
import { loadScreens, screenImage, captureScreen, splitScreen, listMonitors, allowScreenshots, importScreen, setRegions, renameScreen, removeScreen, blueprint, clickRegion } from "./screens.mjs";
|
|
28
|
-
import { mapPage, wholePage, pressRegion, typeRegion, uploadRegion, scrollPage, signIn, keepBrowserOpen, isTrusted, trustedSites, trustSite, untrustSite } from "./headless.mjs";
|
|
28
|
+
import { mapPage, wholePage, pressRegion, typeRegion, uploadRegion, scrollPage, signIn, keepBrowserOpen, isTrusted, trustedSites, trustSite, untrustSite, openSymbiotBrowser, closeSymbiotBrowser, setBrowserHub, siteUrl } from "./headless.mjs";
|
|
29
29
|
import { weeklyState, setWeekly, runWeekly, startWeekly, autostartState, setAutostart, installLauncher, iconSvg, setLauncherLook } from "./desktop.mjs";
|
|
30
30
|
import { markNews, newsSince, watchState, addWatch, setEvery, removeWatch, clearNews, seenWatch, checkWatch, startWatches, setBrief, draftReply, openChat, watchBoard, boardChat, clearBoardChat } from "./watch.mjs";
|
|
31
31
|
import { linksState, linkSite, checkLink, unlinkSite } from "./links.mjs";
|
|
@@ -224,6 +224,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
224
224
|
if (!process.env.SYMBIOT_FORCE_NEW && !RELAUNCH) {
|
|
225
225
|
const p = await askRunningApp("ping", { port: PORT, token: TOKEN });
|
|
226
226
|
if (p && p.version) {
|
|
227
|
+
setBrowserHub(`http://127.0.0.1:${PORT}/browser?t=${TOKEN}`);
|
|
227
228
|
const url = `http://127.0.0.1:${PORT}/?t=${TOKEN}`; const how = process.env.SYMBIOT_NO_OPEN === "1" ? "" : openApp(url);
|
|
228
229
|
console.log(`\n${c.g("●")} ${c.b("Symbiot")} is already running (v${p.version}) at ${c.b(url)}`);
|
|
229
230
|
console.log(how ? c.d(` Opened the existing window (${/^[aeiou]/.test(how) ? "an" : "a"} ${how}).`) : c.d(" Open that URL in your browser."));
|
|
@@ -251,6 +252,8 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
251
252
|
const t0 = Date.now(); res.on("finish", () => { const ms = Date.now() - t0; if (ms > 1500) console.log(`slow: ${String(req.url || "").split("?")[0]} took ${(ms / 1000).toFixed(1)}s`); });
|
|
252
253
|
const u = new URL(req.url, "http://127.0.0.1");
|
|
253
254
|
if (req.method === "GET" && u.pathname === "/") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(EMBEDDED_UI); return; }
|
|
255
|
+
// the Symbiot Browser's own page (headless.mjs openSymbiotBrowser): like /, it gets its token in ?t=
|
|
256
|
+
if (req.method === "GET" && u.pathname === "/browser") { try { res.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" }); res.end(readFileSync(new URL("./browser.html", import.meta.url))); } catch { res.writeHead(404); res.end(); } return; }
|
|
254
257
|
// the app's typeface ships in the package (fonts/), so it's there offline
|
|
255
258
|
// the window's own icon (its taskbar entry), the same orb as the app menu's
|
|
256
259
|
if (req.method === "GET" && (u.pathname === "/favicon.svg" || u.pathname === "/favicon.ico")) {
|
|
@@ -378,7 +381,9 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
378
381
|
if (u.pathname === "/api/mind/forget" && req.method === "POST") { const b = await readBody(req); return json(res, forget(String(b.id || ""))); }
|
|
379
382
|
// Links (links.mjs): one click per standard work site: sign in, trust it, watch it.
|
|
380
383
|
if (u.pathname === "/api/links") { let reach = null; try { reach = linkReach(); } catch {} return json(res, { ...linksState(), ...(reach ? { reach } : {}) }); } // reach: which sites agent runs can use too (agents.mjs)
|
|
381
|
-
if (u.pathname === "/api/links/link" && req.method === "POST") { const b = await readBody(req); return json(res, await linkSite(String(b.id || ""))); }
|
|
384
|
+
if (u.pathname === "/api/links/link" && req.method === "POST") { const b = await readBody(req); return json(res, await linkSite(String(b.id || ""), b.here ? { open: async (url) => ({ ok: true, url }) } : undefined)); } // here: from the Symbiot Browser, which opens the site itself
|
|
385
|
+
if (u.pathname === "/api/browser/open" && req.method === "POST") { const b = await readBody(req); if (process.env.SYMBIOT_NO_OPEN === "1") return json(res, { ok: false, note: "not opened (SYMBIOT_NO_OPEN)" }); return json(res, await openSymbiotBrowser(b.url ? siteUrl(String(b.url)) : "")); }
|
|
386
|
+
if (u.pathname === "/api/browser/done" && req.method === "POST") { if (process.env.SYMBIOT_NO_OPEN === "1") return json(res, { closed: 0 }); const r = await closeSymbiotBrowser(); setTimeout(() => { for (const it of linksState().items) if (it.state !== "off" && it.state !== "ok") checkLink(it.id).catch(() => {}); }, 1500); return json(res, { ...r, checking: true }); }
|
|
382
387
|
if (u.pathname === "/api/links/check" && req.method === "POST") { const b = await readBody(req); return json(res, await checkLink(String(b.id || ""))); }
|
|
383
388
|
if (u.pathname === "/api/links/unlink" && req.method === "POST") { const b = await readBody(req); return json(res, unlinkSite(String(b.id || ""))); }
|
|
384
389
|
// Posts (post.mjs): the week's drafts, waiting on you. Approve is your yes: Marketing's
|
package/ui.mjs
CHANGED
|
@@ -1226,6 +1226,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
|
|
|
1226
1226
|
</div>
|
|
1227
1227
|
</div>
|
|
1228
1228
|
<div class="sset">
|
|
1229
|
+
<div class="row" id="sbrow" style="margin-bottom:12px"><span style="flex:1"><b>The Symbiot Browser</b><br><span class="muted">Symbiot’s own browser window, where you sign in to your sites once so Symbiot can read them for you.</span></span><button class="act" id="sbopen">Open the Symbiot Browser</button></div>
|
|
1229
1230
|
<div id="links"></div>
|
|
1230
1231
|
<div id="linksmsg"></div>
|
|
1231
1232
|
<div class="note muted" id="linksnote">For a whole team, one <b>links.json</b> in Symbiot's config folder adds your company's own sites and hides the ones you don't use, so everyone gets the same buttons.</div>
|
|
@@ -2008,15 +2009,16 @@ var rc=reachOf(d,it.id),rt=rc?(rc.ready?" Your agent's runs can use it too, thro
|
|
|
2008
2009
|
return "<div class='lnk cx "+escQ(it.state)+"' data-id='"+escQ(it.id)+"'><button type='button' class='lbtn' title='"+escQ(String(it.note||(on?'open '+it.url+' to sign in again':'sign in at '+it.url+' and link it')).replace(/\.?\s*$/,rt?'.':'')+rt)+"'><span class='cxb' aria-hidden='true'></span><span class='cxn'>"+esc(it.name)+"</span>"+(rc?"<i class='ic-plug cxa"+(rc.ready?" ready":"")+"' role='img' aria-label='"+escQ(rc.ready?"agent runs can use it too":"agent runs can use it once connected in claude.ai")+"'></i>":"")+"<span class='cxs'>"+(on?esc(LINKWORD[it.state]||''):'Link')+"</span></button>"+
|
|
2009
2010
|
(on?"<button type='button' class='lmore' title='check it now' aria-label='Check "+escQ(it.name)+" now'>↻</button><button type='button' class='lrm' title='unlink: stop watching it and stop trusting it'>Unlink</button>":"")+"</div>";}).join('')+"</div>";}).join('')+"</div>";return h;}
|
|
2010
2011
|
function linksOut(html){var o=document.getElementById('linksmsg');if(o)o.innerHTML=html;var b=document.getElementById('boardmsg');if(b&&LINKS&&!LINKS.linked)b.innerHTML=html;}
|
|
2012
|
+
function sbOpen(url){api('/api/browser/open',url?{url:url}:{}).then(function(r){if(r&&r.error)linksOut("<div class='note err'>"+esc(r.error)+"</div>");}).catch(function(){});}
|
|
2011
2013
|
function renderLinks(){if(!LINKS)return;['links','boardlinks'].forEach(function(id){var el=document.getElementById(id);if(!el)return;
|
|
2012
2014
|
el.innerHTML=linksHtml(LINKS,id==='boardlinks');
|
|
2013
2015
|
el.querySelectorAll('.lnk').forEach(function(sp){var lid=sp.getAttribute('data-id'),nm=sp.querySelector('.cxn'),name=nm?nm.textContent:sp.querySelector('.lbtn').textContent;
|
|
2014
2016
|
sp.querySelector('.lbtn').addEventListener('click',function(){linksOut("<div class='note muted'>Opening "+esc(name)+"…</div>");
|
|
2015
2017
|
api('/api/links/link',{id:lid}).then(function(r){if(!r||r.error){linksOut("<div class='note err'>"+esc(name)+": "+esc((r&&r.error)||'failed')+"</div>");loadLinks();return;}
|
|
2016
|
-
linksOut("<div class='note ok'>Opened "+esc(name)+"
|
|
2018
|
+
linksOut("<div class='note ok'>Opened the Symbiot Browser with "+esc(name)+". Sign in there as you normally do, then click Done in the Symbiot Browser: Symbiot checks it within a minute.</div>");loadLinks();
|
|
2017
2019
|
[20000,60000,120000].forEach(function(ms){setTimeout(loadLinks,ms);});});});
|
|
2018
2020
|
var m=sp.querySelector('.lmore');if(m)m.addEventListener('click',function(){m.disabled=true;api('/api/links/check',{id:lid}).then(function(r){m.disabled=false;
|
|
2019
|
-
if(r&&r.busy)linksOut("<div class='note muted'>Symbiot
|
|
2021
|
+
if(r&&r.busy)linksOut("<div class='note muted'>The Symbiot Browser is open. Click Done in it when you’ve signed in, then check again.</div>");else if(r&&r.error)linksOut("<div class='note err'>"+esc(r.error)+"</div>");loadLinks();loadBoard();});});
|
|
2020
2022
|
var x=sp.querySelector('.lrm');if(x)x.addEventListener('click',function(){api('/api/links/unlink',{id:lid}).then(function(){linksOut('');loadLinks();loadTrusted();loadBoard();});});});});}
|
|
2021
2023
|
// What Symbiot remembers (mind.mjs): a row per thing, with what it knows; × forgets one.
|
|
2022
2024
|
function loadMind(){api('/api/mind').then(function(d){var el=document.getElementById('mindlist');if(!el||!d)return;var ns=d.nodes||[];
|
|
@@ -2183,7 +2185,7 @@ var DRAFTING_CHAT="✓ Your agent is drafting a reply in that chat. It open
|
|
|
2183
2185
|
var OPEN_WA_TIP="open WhatsApp in Symbiot's browser, where the chat shows your agent's reply in its message box, for you to read and send";
|
|
2184
2186
|
function openChatUI(n,b,say){if(!n)return;b.disabled=true;
|
|
2185
2187
|
api('/api/watch/open-chat',{id:n.id}).then(function(x){b.disabled=false;if(!x||x.error){say('err',esc((x&&x.error)||'failed'));return;}
|
|
2186
|
-
say('ok',"✓ Opened WhatsApp in Symbiot
|
|
2188
|
+
say('ok',"✓ Opened WhatsApp in the Symbiot Browser. The chat shows your agent's reply in its message box: read it, change it if you like, and send it there. Close the window when you're done, so Watch can read WhatsApp again.");}).catch(function(e){b.disabled=false;say('err',esc(String((e&&e.message)||e)));});}
|
|
2187
2189
|
var DRAFTING="✓ Your agent is drafting a reply to it. It opens the email in your inbox through Screens, writes the reply and leaves it in Drafts for you to read and send: it never presses Send. Follow it, and answer anything it asks, in the Agents tab.";
|
|
2188
2190
|
// a LinkedIn comment or mention (watch.mjs socialBrief): typed into the comment box, never posted
|
|
2189
2191
|
var DRAFT_SOCIAL_TIP="your coding agent opens it on LinkedIn in Symbiot's hidden browser and types a reply into the comment box, unposted. It never presses Post, Comment, Reply or Send";
|
|
@@ -3543,5 +3545,5 @@ var G=lqGL();if(G){try{lqStep();lqDraw(G);}catch(e){}}if(typeof window.requestAn
|
|
|
3543
3545
|
function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0)lqStep();var every=(GLSOFT||GLSTILL)?glEvery(LQ.mode==='aware'):LQ.mode==='pool'?6:LQ.mode==='rest'?2:1;if(G&&LQ.frame%every===0)lqDraw(G);}raf(frame);} // moves every frame; drawn less at rest, behind a panel, without a GPU
|
|
3544
3546
|
raf(frame);}
|
|
3545
3547
|
|
|
3546
|
-
initGraphEvents();syncP();refresh();firstTab();lqInit();onbLoad();var rr=$('rerunsetup');if(rr)rr.addEventListener('click',function(){api('/api/onboarding/restart',{}).then(function(){lqSink();onbLoad();});});awayInit();lqDeep();loadWhatsNew();loadAgentCfg();loadScanRoots();loadKnowledge();loadPhone();loadLinks();loadMind();loadTrusted();loadMail();loadScreensUI();loadMonitorsUI();loadDesktop();loadWatchUI();setInterval(function(){whenFree(document.querySelector('main'),'watch',loadWatchUI);},60000);loadPhoneLink();
|
|
3548
|
+
initGraphEvents();syncP();refresh();firstTab();lqInit();onbLoad();var sbo=$('sbopen');if(sbo)sbo.addEventListener('click',function(){sbOpen('');});var rr=$('rerunsetup');if(rr)rr.addEventListener('click',function(){api('/api/onboarding/restart',{}).then(function(){lqSink();onbLoad();});});awayInit();lqDeep();loadWhatsNew();loadAgentCfg();loadScanRoots();loadKnowledge();loadPhone();loadLinks();loadMind();loadTrusted();loadMail();loadScreensUI();loadMonitorsUI();loadDesktop();loadWatchUI();setInterval(function(){whenFree(document.querySelector('main'),'watch',loadWatchUI);},60000);loadPhoneLink();
|
|
3547
3549
|
</script></body></html>`;
|