symbiot 0.63.6 → 0.63.7
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 +36 -0
- package/README.md +39 -5
- package/agents.mjs +43 -10
- package/browser.html +21 -3
- package/capture.mjs +761 -0
- package/demo.mjs +171 -0
- package/guard.mjs +23 -5
- package/handback.mjs +22 -8
- package/handover.mjs +1 -1
- package/headless.mjs +281 -80
- package/home.mjs +26 -5
- package/index.mjs +65 -11
- package/lanes.mjs +99 -9
- package/links.mjs +52 -2
- package/marketing.mjs +40 -7
- package/mdns.mjs +6 -4
- package/package.json +5 -3
- package/phone.mjs +78 -11
- package/reports.mjs +15 -1
- package/server.mjs +89 -42
- package/tray.mjs +44 -4
- package/ui.mjs +311 -41
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,42 @@
|
|
|
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.63.7 — 2026-10-12
|
|
6
|
+
|
|
7
|
+
- **`symbiot capture Symbiot` works in one go.** It used to refuse, because your running Symbiot shows your real work, and needed a copy started by hand. It now starts a throwaway Symbiot with a made-up company in it (Acme: its repos, its agents' questions, what arrived, its week, a Marketing draft), captures Home, the Workdesk, the Dashboard, the Week, the Map and Marketing, then stops it and deletes it. Symbiot's own tray can be brought up to date after every release.
|
|
8
|
+
- **Captures no longer blur a page's backdrop.** A drawing behind the whole page (Symbiot's liquid) was taken for a picture that might show faces, so every capture of Symbiot came out blurred end to end. A page's live title isn't taken for a notification either.
|
|
9
|
+
- **Your computer's reports on your phone.** The phone's Reports opened only the phone's own, which was empty. The copy your computer sends the phone now carries its 5 newest reports, so you can read an audit or a plan away from your desk, offline too. Each says which computer it's from. A long one comes in its first part, which says the rest is on your computer.
|
|
10
|
+
- **A Reddit draft previews as a Reddit post.** Marketing showed every draft as a LinkedIn post. A Reddit one now shows as Reddit would: its subreddit, who posts it, its title, its flair, then the body, with `code` set as code. It counts the title against Reddit's 300 characters.
|
|
11
|
+
- **Each agent gets its own tab in Symbiot's hidden browser.** Two agents using `symbiot screens` at once (one on LinkedIn, one in the Twilio console) shared one page, so a scheduled post being written lost its text and picture between steps, and one agent's typing could land on the other's page. Now each agent's next step carries on from its own page, just as it left it.
|
|
12
|
+
- **"← All reports" stays in view on a long report.** It no longer scrolls away with the report: the bar with it, and the report's path where it fits, stays pinned under the Reports header, so the list is one click away from anywhere in it.
|
|
13
|
+
- **A draft post opens large on Marketing.** Click a draft (in Needs you, in what its agent drafted, its preview, or a card on the posts board) and it opens over the page: its pictures and video near full size, its whole text in reading type, which product it's for and where it goes (LinkedIn, or r/ClaudeAI with its flair). Click a picture to see it alone, and go through a post's pictures with the arrow keys or a swipe. Approve, Edit and Skip work right there. Esc, the X or a click outside closes it. The pick tray's captures open large the same way.
|
|
14
|
+
- **A handover the loop guard stops still reaches its lane.** After four handovers in a row, Symbiot doesn't start another agent, so two lanes can't pass the same thing back and forth. It used to drop the handover there, so ops' User-Agent fix for this repo was only found by reading `lanes.json`. It now lands in the receiving repo's tasks for its next run, and the lane that sent it sees why it didn't start.
|
|
15
|
+
- **Agents can change one line of a post without retyping it.** `symbiot screens type <id> <field> --replace "old" "new"` types over only that part of a field, so the rest stays as it is. Retyping a scheduled LinkedIn post to change one word had dropped its picture. If the words aren't there, or are there more than once, nothing changes and it says so.
|
|
16
|
+
- **Symbiot records a desktop app's window under Wayland too.** On COSMIC, GNOME or KDE, a window that isn't running under XWayland used to be out of reach for `symbiot capture --window`. It now goes through your desktop's own screen sharing: you pick the window in its prompt the first time (a notification says which one), and Symbiot remembers it, so later captures of it don't ask.
|
|
17
|
+
- **Your phone finds your computer on your Wi-Fi, even with a VPN on.** With Tailscale (or another VPN) running, your computer told the phone on your network it was at the VPN's address, which the phone can't reach, so it couldn't find the computer again after the router handed out new addresses. It now answers each phone with the address on that phone's own network, and never gives out a VPN's.
|
|
18
|
+
- **Agents can pick from a website's dropdown.** `symbiot screens choose <id> <menu> "<option>"` picks an option in a native `<select>` (a DNS record's type, say), and the fields that depend on it update, like the Priority box an MX record turns on.
|
|
19
|
+
- The npm package and the installers now list garth@symbiot.co.za as the author.
|
|
20
|
+
- **Your phone's Home is what you act on there.** Paired with your computer, it no longer opens on the liquid with your computer's work behind a droplet: it's a plain list, with what needs you there first (its agents' questions to answer, its work to Approve), then its tasks to tick or add, then what Watch found. What the phone itself needs sits under your computer's, its setup in one row. Settings, Reports, the Dashboard and the rest are a row along the top, one tap each. On a phone-sized screen the header fits on one line too: the look switch isn't cut off any more.
|
|
21
|
+
- **Symbiot only answers for you when the answer is purely an action.** A question whose recommended option says something before its agent tag ("Yes, it's through. Agent: …"), or has no tag at all, states a fact only you know, so it comes to you instead of being picked for you.
|
|
22
|
+
- **A handover says where it stands, on the block of the lane that sent it.** Each one shows whether its lane has it yet and, if not, why: not picked up yet, queued behind a busy agent, waiting for the run on its earlier version to end, or couldn't go. Once it's with its lane, you see how far that agent has got (tasks ticked, what it's doing now) until its answer comes back. It shows in the Agents tab and on the project on the Workdesk.
|
|
23
|
+
- **A sent handover no longer looks unsent.** Once everything in a lane's `.symbiot/HANDOFF.md` has gone, Symbiot rewrites the file as a dated list of what went where. whatsapp_module's agent had seen its file still there and told you it hadn't been sent, while ops had both its handovers running.
|
|
24
|
+
- **Answering a question on Home works again, and you see it work.** A card that shows a question in plain words (a permission ask with Allow, or one naming a file) sent those plain words back, which matched no question, so Allow and Send did nothing and the error only showed in Home's talk. Its answer now goes under the agent's own question. A click shows at once (the button pressed, "Sending…"), then the card goes, or says on itself what went wrong. Pressing it again doesn't add a second answer, and a card you've answered doesn't come back while Home catches up. A long question your phone shows cut short is still found too.
|
|
25
|
+
- **A question about work that's since been done says so.** When a lane asks about something it handed over, and the lane that took it has finished it, its card says who did it and puts Close it first. whatsapp_module kept asking whether it could put the WhatsApp module back on sts-prod after ops had already done it.
|
|
26
|
+
- **Done in the Symbiot Browser closes it, every time.** Done could find nothing to close when Symbiot had been started from a terminal, so the window stayed open and no site was checked, however often you pressed it. It now closes every window of it, says at once that it's closing, and tells you if Symbiot isn't answering. Closing it with its X does the same as Done: Symbiot checks each site within a minute. A site the browser can't read yet is tried again a little later, instead of being skipped.
|
|
27
|
+
- **Reddit's sign-in is read right.** Signed out, Reddit's home page has no password box, only a Log In button, so Symbiot couldn't tell the two apart. A page whose own button is just "Log in" or "Sign in" now counts as signed out. Once a site you were asked to sign in to reads signed in, the agent's question about it is answered for you and that agent carries on, so Marketing's "sign in to www.reddit.com" clears by itself.
|
|
28
|
+
- **Agents can use Screens while the app runs.** Every map, press and type failed with "The Symbiot Browser is open: click Done", with no window open: a hidden Chrome on Symbiot's browser profile that the app hadn't started itself was taken for the Symbiot Browser. Symbiot now reads whether the Chrome holding the profile is hidden. A hidden one left by another Symbiot (a one-off command, or an app from before an update) gets a few seconds to finish, then is closed.
|
|
29
|
+
- **Symbiot captures your product for its posts by itself, so you don't need a demo video.** **Capture a product** on the Marketing page, or `symbiot capture <product>`, opens it in a browser of its own, works out its key screens from its routes and README, and takes a picture of each and short clips (MP4 and GIF). A desktop app's window is recorded instead. Marketing's agent does this on a product's first run.
|
|
30
|
+
- **Nothing private reaches the pick tray unblurred.** Before a capture lands there, Symbiot blurs names, emails, phone numbers, addresses, keys and tokens, account and card numbers, what's typed into forms, notifications, chats, avatars and pictures, and text inside pictures. The Marketing page shows each box. Switch any off, or draw your own over anything it missed. A desktop recording says to look it over first, because Symbiot can't see faces in a window, and it can't go on a post until you've clicked **Looked it over**. The unblurred originals are never shown and never posted.
|
|
31
|
+
- Added a check that a step naming a file is done only once that file has actually changed.
|
|
32
|
+
- Changed the update process to wait and retry when a newly listed version can't be downloaded yet.
|
|
33
|
+
- Fixed Symbiot failing to update on Linux.
|
|
34
|
+
- Fixed the reply watcher counting your own sent email as the reply you were waiting for.
|
|
35
|
+
- Fixed the reply watcher firing on inbox rows where the newest message is your own.
|
|
36
|
+
- Changed Symbiot's guard to let agents read public SSH keys, while private keys and everything else there stay blocked.
|
|
37
|
+
- Changed pairing so scanning the code on your computer opens the Android app straight away, without the website first.
|
|
38
|
+
- Changed the phone relay setup for symbiot.co.za to retry once the Cloudflare token has the permissions it needs.
|
|
39
|
+
- Added reaching your computer from your phone away from home through a relay that only passes sealed messages.
|
|
40
|
+
|
|
5
41
|
## 0.63.6
|
|
6
42
|
|
|
7
43
|
- **Approve opens on what you came for.** Clicking Review and approve on Home showed every project's work waiting for review, each task in full, with the Approve button several screens down. Now you see only that project, with its Approve button straight under its name, and its tasks one line each: the first three, with the rest a click away. Click a task to read it all. The other projects waiting on you are one line, with a button to show them.
|
package/README.md
CHANGED
|
@@ -300,8 +300,8 @@ in the repo's `SYMBIOT_KEYSTORE_B64` and `SYMBIOT_KEYSTORE_PASS` secrets (see
|
|
|
300
300
|
### Your phone
|
|
301
301
|
|
|
302
302
|
Your computer's work, on your phone: your tasks, what needs you, your agents'
|
|
303
|
-
questions, the work waiting for your Approve, and what Watch found
|
|
304
|
-
notification too
|
|
303
|
+
questions, the work waiting for your Approve, and what Watch found, each as a
|
|
304
|
+
notification too when it's new. Experimental.
|
|
305
305
|
|
|
306
306
|
- **Pair it.** On the computer, Setup's **Your phone** step (or Settings → **Your
|
|
307
307
|
phone**) switches the link on and shows a QR. Point your phone's camera at it and
|
|
@@ -979,6 +979,35 @@ launches or pricing; any agent can hand work to it too, as `marketing`.
|
|
|
979
979
|
shows as the agent's option ("🤖 Agent: …, once you approve its preview"). Only
|
|
980
980
|
signing in, for a platform that isn't linked or has signed out, is yours. It never
|
|
981
981
|
posts what you haven't approved, and never signs in as you.
|
|
982
|
+
- **Symbiot captures your product itself: no demo video needed.** **Capture a
|
|
983
|
+
product** on the Marketing page (or `symbiot capture <product>`, which its agent runs
|
|
984
|
+
on a product's first run) opens it in a browser of its own, with a fresh profile, so
|
|
985
|
+
nothing you're signed in to shows. It works out the key screens from the product's
|
|
986
|
+
routes (Next, Nuxt, SvelteKit, Astro, Remix, Express and the like, React and Vue
|
|
987
|
+
Router, Flask, FastAPI, Django, Rails) and the addresses its README gives, and takes
|
|
988
|
+
a picture of each and a short clip of the first few (MP4 and GIF, and a tour of them
|
|
989
|
+
one after another). Its address: the one you give, else the app of it running here;
|
|
990
|
+
start it with its demo data if it has some. Symbiot itself is captured from a copy
|
|
991
|
+
with made-up work in it (Acme, a company that doesn't exist), started for the capture
|
|
992
|
+
and stopped after it, never your own app with your work in it: `symbiot capture
|
|
993
|
+
Symbiot` brings its tray up to date in one go. A desktop app's window is recorded
|
|
994
|
+
instead (`--window "<title>"`: an X11 or XWayland window on Linux by its title; a
|
|
995
|
+
Wayland window (COSMIC, GNOME, KDE) through the desktop's own screen sharing, where
|
|
996
|
+
you pick it in its prompt once and Symbiot remembers it; by its title on Windows; the
|
|
997
|
+
screen on a Mac).
|
|
998
|
+
- **What could get you into trouble is blurred before it reaches the tray**: names
|
|
999
|
+
(the people Symbiot remembers, the product's git authors, the one beside an avatar,
|
|
1000
|
+
a customer's in a table, and any in the lane's `.symbiot/names.json`), emails, phone
|
|
1001
|
+
numbers, street addresses, API keys, tokens and secrets, account, card and IBAN
|
|
1002
|
+
numbers, what's typed into forms, notifications, chats, avatars and pictures (faces),
|
|
1003
|
+
and text inside pictures (read with `tesseract`, where it's installed). A clip moves
|
|
1004
|
+
a window at a time, so what's private is found in each moment it shows. Each
|
|
1005
|
+
capture's boxes are listed on it: switch one off, or **Draw a blur box** over
|
|
1006
|
+
anything it missed (a box you drew can be removed). A desktop recording, where
|
|
1007
|
+
Symbiot can't see faces or pictures, and any capture whose pictures' text couldn't
|
|
1008
|
+
be read, says so, and goes on a post only once you've clicked **Looked it over**.
|
|
1009
|
+
The unblurred originals stay in the lane's `.symbiot/tray-originals/`: never shown,
|
|
1010
|
+
never put on a post.
|
|
982
1011
|
- **Its work waits for your OK like any project's** (**Approve** commits it in its
|
|
983
1012
|
folder), and its questions reach you like any agent's. Answering one says
|
|
984
1013
|
**Sent to Marketing**, the lane it went to.
|
|
@@ -1319,6 +1348,8 @@ symbiot screens type <id> "Search mail" "invoice" --enter --yes # type into a
|
|
|
1319
1348
|
# (--yes isn't needed on a site under Trusted sites in Settings)
|
|
1320
1349
|
symbiot screens type <id> To "sam@example.com" --yes # without --enter: the text stays in the field...
|
|
1321
1350
|
symbiot screens press <new id> Send --yes # ...for a press on the screen that just printed
|
|
1351
|
+
symbiot screens type <id> "Post text" --replace "Monday" "Tuesday" --yes # change only that part of a field:
|
|
1352
|
+
# the rest, a post's picture too, stays as it is
|
|
1322
1353
|
symbiot screens upload <id> "Add media" ~/clip.mp4 --yes # put a file in the page's file box: the field,
|
|
1323
1354
|
# or the button a site hides it behind ("used" says which box)
|
|
1324
1355
|
symbiot screens scroll <id> # "more": "below"? scroll down and map the next part (or up, top, bottom)
|
|
@@ -1429,8 +1460,9 @@ what's next.
|
|
|
1429
1460
|
**On your phone.** Your phone can show your computer's work, through Symbiot
|
|
1430
1461
|
there (the [Android app](#the-android-app-apk), or Symbiot in
|
|
1431
1462
|
[Termux](#on-your-phone-android-in-termux)): your tasks, what needs you, your
|
|
1432
|
-
agents' questions, the work waiting for your Approve, and what Watch found
|
|
1433
|
-
|
|
1463
|
+
agents' questions, the work waiting for your Approve, and what Watch found. Each
|
|
1464
|
+
of those comes as a notification too, when it's new, so nothing waits on you
|
|
1465
|
+
until you happen to open the app. From the phone you can add and tick tasks, answer
|
|
1434
1466
|
your agents and approve. See [Your phone](#your-phone) for how it works.
|
|
1435
1467
|
|
|
1436
1468
|
**More than one display?** Symbiot reads how your displays are laid out
|
|
@@ -1472,7 +1504,9 @@ its own browser. **Settings → Links → Open the Symbiot Browser** (or `symbio
|
|
|
1472
1504
|
opens it: a window of its own listing your sites, each with whether you're signed in and
|
|
1473
1505
|
a **Sign in** button, plus an address bar for any other site. Sign in the usual way
|
|
1474
1506
|
(the site's own login and 2FA; Symbiot never sees a password), then click **Done**: it
|
|
1475
|
-
closes and Symbiot checks each site.
|
|
1507
|
+
closes and Symbiot checks each site. Closing it with its X does the same. An agent's
|
|
1508
|
+
question asking you to sign in to a site is answered for you once that site reads
|
|
1509
|
+
signed in, and the agent carries on.
|
|
1476
1510
|
|
|
1477
1511
|
## Any language
|
|
1478
1512
|
|
package/agents.mjs
CHANGED
|
@@ -286,7 +286,8 @@ function runHandoff(repoPath, { force = false, lost = false } = {}) {
|
|
|
286
286
|
const lock = join(repoPath, ".symbiot", LOCK);
|
|
287
287
|
const own = opts.env && typeof opts.env === "object" ? Object.fromEntries(Object.entries(opts.env).map(([k, v]) => [k, String(v)])) : null, boxEnv = sandboxEnv(box);
|
|
288
288
|
const owner = licenceState().plan === "owner" ? { SYMBIOT_OWNER: "1" } : null; // the guard lets only the owner's runs touch Symbiot's own files
|
|
289
|
-
|
|
289
|
+
// SYMBIOT_AGENT: which run a `symbiot screens` command comes from, so it gets its own tab in the app's hidden browser
|
|
290
|
+
const env = { ...(own || {}), ...(boxEnv || {}), ...(owner || {}), SYMBIOT_AGENT: repoPath };
|
|
290
291
|
const e = track(typeof opts.name === "string" && opts.name ? opts.name.slice(0, 80) : repoPath.split("/").pop(), fillHandoff(runCmd, repoPath, resume ? RESUME_PROMPT : HANDOFF_PROMPT), repoPath, (code) => {
|
|
291
292
|
if (noteSession(repoPath, code, !!resume) === "lost") { setTimeout(() => { try { runHandoff(repoPath, { force: true, lost: true }); } catch {} }, 300); return; } // its conversation is gone: start afresh
|
|
292
293
|
try { if (JSON.parse(readFileSync(lock, "utf8")).pid === e.pid) unlinkSync(lock); } catch {}
|
|
@@ -661,6 +662,15 @@ function namedFiles(text, dir) {
|
|
|
661
662
|
return out.filter((f, i) => out.findIndex((g) => g.path === f.path) === i).slice(0, 5);
|
|
662
663
|
}
|
|
663
664
|
const fileSig = (p) => { try { const s = statSync(p); return s.mtimeMs + ":" + s.size; } catch { return "none"; } };
|
|
665
|
+
// A step of yours that names files: the ones it names, if none has changed since
|
|
666
|
+
// `since` (the run that asked ended), each { name, at: its last change, 0 if it
|
|
667
|
+
// isn't there }; [] when it names none or one has changed. Done on its card started
|
|
668
|
+
// a run told "Done: the user did it" while the `.env` it named was last changed on
|
|
669
|
+
// 2026-10-06, days before the run asked.
|
|
670
|
+
function unchangedStep(text, dir, since) {
|
|
671
|
+
const files = namedFiles(text, dir).map((f) => { let at = 0; try { at = statSync(f.path).mtimeMs; } catch {} return { name: f.name, at }; });
|
|
672
|
+
return files.length && !files.some((f) => f.at > since) ? files : [];
|
|
673
|
+
}
|
|
664
674
|
// What a folder is waiting on: { step, files: [names], cmd, at, rerun }, or null
|
|
665
675
|
// once a file it names, or the agent command, has changed. Then waiting.json
|
|
666
676
|
// goes, unless a run was asked for: startWaiting needs it to start that one (the
|
|
@@ -804,12 +814,18 @@ function droppedTasks(path) {
|
|
|
804
814
|
// Swap the held brief in, keeping the ticks the agent made meanwhile: a tick
|
|
805
815
|
// is how a task reaches review, so dropping one would lose that task's work.
|
|
806
816
|
// A task the agent deleted meanwhile stays out, and stays deleted in handed.json.
|
|
817
|
+
// A tick with a note after it ("… — Done: the card sent …", as the brief asks for a
|
|
818
|
+
// stale one) is the same task: an exact match lost the Needs-you fix's tick when the
|
|
819
|
+
// marketing handover was held behind its run, and the task came back open (2026-10-12).
|
|
820
|
+
// The agent's own line, note and all, is what lands.
|
|
807
821
|
function releaseHeldTasks(path) {
|
|
808
822
|
const held = readSymbiot(path, HELD); if (!held || runningHandoff(path)) return false;
|
|
809
823
|
const isTick = /^\s*-\s*\[x\]\s*/i, key = (l) => l.replace(/^\s*-\s*\[[ x]\]\s*/i, "").trim().toLowerCase();
|
|
810
|
-
const ticked =
|
|
824
|
+
const ticked = readSymbiot(path, "TASKS.md").split("\n").filter((l) => isTick.test(l)).map((l) => ({ k: key(l), line: l }));
|
|
825
|
+
const noted = (t, k) => t.startsWith(k) && /^\s*(?:—|–|-{1,2}|:|\(|\.\s)/.test(t.slice(k.length));
|
|
826
|
+
const tickOf = (k) => ticked.find((t) => t.k === k) || ticked.find((t) => noted(t.k, k) || sameTask(t.k, k));
|
|
811
827
|
const gone = droppedTasks(path).map((d) => d.text), isGone = (l) => /^\s*-\s*\[ \]/.test(l) && gone.some((g) => sameTask(g, key(l)));
|
|
812
|
-
const md = held.split("\n").filter((l) => !isGone(l)).map((l) => /^\s*-\s*\[ \]/.test(l) &&
|
|
828
|
+
const md = held.split("\n").filter((l) => !isGone(l)).map((l) => { const t = /^\s*-\s*\[ \]/.test(l) && tickOf(key(l)); return t ? l.match(/^\s*/)[0] + t.line.trimStart() : l; }).join("\n");
|
|
813
829
|
try { writeFileSync(join(path, ".symbiot", "TASKS.md"), md); unlinkSync(join(path, ".symbiot", HELD)); noteHanded(path, md, gone); return true; } catch { return false; }
|
|
814
830
|
}
|
|
815
831
|
// Land the held brief and, if it leaves anything open, start an agent on it.
|
|
@@ -1195,11 +1211,15 @@ function autoAllow(path) {
|
|
|
1195
1211
|
// A question whose answer the agent already knows isn't the user's: when its own
|
|
1196
1212
|
// recommended option is a step it takes itself (not 👤), and nothing in the question or
|
|
1197
1213
|
// that option is an approval, goes out, costs money, touches an account or a machine, or
|
|
1198
|
-
// can't be undone, Symbiot picks it and the agent carries on.
|
|
1214
|
+
// can't be undone, Symbiot picks it and the agent carries on. Only a pure action, though:
|
|
1215
|
+
// an option that says anything before its 🤖 tag ("Yes, it's through. 🤖 Agent: …"), or
|
|
1216
|
+
// has no tag at all, states a fact or a choice only the user knows, so it goes to them.
|
|
1217
|
+
// Everything else still comes to the user.
|
|
1218
|
+
const pureAction = (o) => /^[\s*_]*🤖/u.test(String(o || ""));
|
|
1199
1219
|
const OUTWARD = /\b(approv\w*|publish\w*|releas\w*|push\w*|merg\w*|send\w*|sent|post\w*|repl\w*|e-?mail\w*|pay\w*|spend\w*|buy\w*|money|bill\w*|cost\w*|delet\w*|remov\w*|uninstall\w*|wip\w*|drop\w*|password\w*|2fa|token\w*|keys?|sign(?:ed|s)?[- ]?in|log(?:ged)?[- ]?in|accounts?|subscription|plan|tunnel\w*|prox\w*|ssh|vm|servers?|restart\w*|stop\w*|kill\w*|broker\w*|trad\w*|bank\w*|npm|deploy\w*|install\w*|phone|whatsapp|linkedin|github\.com|live|production|prod)\b|\byour (?:default|settings?|profile)\b/i;
|
|
1200
1220
|
function autoPick(q) {
|
|
1201
1221
|
const rec = (q.options || []).filter((o) => /\(recommended\b/i.test(o));
|
|
1202
|
-
if (rec.length !== 1 || yourStep(rec[0])) return "";
|
|
1222
|
+
if (rec.length !== 1 || yourStep(rec[0]) || !pureAction(rec[0])) return "";
|
|
1203
1223
|
return OUTWARD.test(`${q.q} ${q.context || ""} ${rec[0]}`) ? "" : rec[0];
|
|
1204
1224
|
}
|
|
1205
1225
|
function autoDecide(path) {
|
|
@@ -1254,15 +1274,28 @@ function grantFromStep(step, path = "", notes = {}) {
|
|
|
1254
1274
|
if (tool && !/^(sudo|rm|dd|mkfs)$/.test(tool)) { const r = grantAgent({ tool }); if (r && r.ok) got.push(tool); }
|
|
1255
1275
|
return got.length ? got : null;
|
|
1256
1276
|
}
|
|
1277
|
+
// The question an answer is for, from qKey -> question: the same words, or the start of
|
|
1278
|
+
// one (a phone shows a long question cut short, ending in "…"), when only one starts so.
|
|
1279
|
+
function findQuestion(qs, q) {
|
|
1280
|
+
const k = qKey(q); if (!k) return "";
|
|
1281
|
+
if (qs.has(k)) return qs.get(k);
|
|
1282
|
+
const cut = k.replace(/\s*(…|\.\.\.)$/, ""); if (cut === k || cut.length < 20) return "";
|
|
1283
|
+
const hits = [...qs.keys()].filter((x) => x.startsWith(cut));
|
|
1284
|
+
return hits.length === 1 ? qs.get(hits[0]) : "";
|
|
1285
|
+
}
|
|
1257
1286
|
const stepText = (a) => String(a).split("🤖")[0].replace(/^\s*👤\s*(You:)?\s*/, "").replace(/\s*\(recommended\)\s*$/i, "").trim();
|
|
1258
1287
|
function answerQuestions(path, answers, opts = {}) {
|
|
1259
1288
|
path = String(path || "");
|
|
1260
1289
|
if (!path || !knownRun(path)) return { error: "No agent has run in that folder." };
|
|
1261
1290
|
const open = new Map(agentQuestions(path, "").questions.map((x) => [qKey(x.q), x.q]));
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
if (!rows.length
|
|
1291
|
+
const given = (Array.isArray(answers) ? answers : []).map((x) => ({ q: String((x && x.q) || ""), a: String((x && x.a) || "").replace(/^\s*#+/gm, "").trim().slice(0, TASK_MAX) })).filter((x) => x.a);
|
|
1292
|
+
const rows = given.map((x) => ({ q: findQuestion(open, x.q), a: x.a })).filter((x) => x.q);
|
|
1293
|
+
// pressed twice, or answered on another screen first: it's in ANSWERS.md already, so nothing is added again
|
|
1294
|
+
if (!rows.length && given.length) {
|
|
1295
|
+
const had = new Map([...readSymbiot(path, "ANSWERS.md").matchAll(/^###\s+(.+)$/gm)].map((m) => [qKey(m[1]), m[1]]));
|
|
1296
|
+
if (given.every((x) => findQuestion(had, x.q))) return { ok: true, saved: 0, already: true, note: "That was answered already: its agent has it." };
|
|
1297
|
+
}
|
|
1298
|
+
if (!rows.length) return { error: given.length ? "That question isn't open any more: its agent changed or closed it. Look again on its block on the Workdesk." : "Pick or type at least one answer." };
|
|
1266
1299
|
const prev = readSymbiot(path, "ANSWERS.md") || "# Answers from the user\nAnswers to the questions in QUESTIONS.md, newest last. Follow them; ask again in QUESTIONS.md if one is unclear.\n";
|
|
1267
1300
|
const day = new Date().toISOString().slice(0, 10);
|
|
1268
1301
|
try {
|
|
@@ -1350,4 +1383,4 @@ function agentsList() {
|
|
|
1350
1383
|
}).concat(earlierRuns());
|
|
1351
1384
|
}
|
|
1352
1385
|
|
|
1353
|
-
export { CHANGES, toKnownHost, continueLanes, autoPick, autoDecide, FACTS, factsOf, knownRun, withStream, workOf, HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, grantRule, allowTool, claudeConnectors, withConnectors, linkedConnectors, connectorsLine, connectorsInfo, linkReach, fillHandoff, runHandoff, nothingToDo, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, waitingFor, startWaiting, noteUntracked, untrackedBefore, forgetUntracked, writeTasks, droppedTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, pickAgent, findOrcaCli, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList, needsOf, settleNeeds, NEEDS_FOR, readLastWords, readNeeds, parseRead, READ_FILE, autoAllow, autoAllowSweep, allowlistInWork, installAllowlist, inWork, withScope, withTrust, sandboxFor, sandboxEnv, sandboxNeeds, sandboxState, sandboxWrites, SANDBOX_DIR, resumeFor, noteSession, trustFull, GUARD_SETTINGS, isUrgent, urgentFirst, urgentUndo, urgentDone, urgentSweep, urgentState , licenceGate };
|
|
1386
|
+
export { CHANGES, toKnownHost, continueLanes, autoPick, autoDecide, FACTS, factsOf, knownRun, withStream, workOf, HANDOFFS, HANDOFF_PROMPT, QUESTIONS_MAX, OPTIONS_SHOWN, IDEAS_SHOWN, shSingle, CLAUDE_CMD, ORCA_CLAUDE_CMD, handoffCmd, setHandoffCmd, grantAgent, grantRule, allowTool, claudeConnectors, withConnectors, linkedConnectors, connectorsLine, connectorsInfo, linkReach, fillHandoff, runHandoff, nothingToDo, PARKED_NOTE, parkedPaths, isParked, parkLane, agentMissing, blockedAgain, runningHandoff, loadRuns, earlierRuns, namedFiles, unchangedStep, waitingFor, startWaiting, noteUntracked, untrackedBefore, forgetUntracked, writeTasks, droppedTasks, releaseHeldTasks, startHeldTasks, detectHandoffs, pickAgent, findOrcaCli, orcaHandoffCmd, migrateOrcaCmd, migrateClaudeCmd, track, agentChanges, parseQuestions, suggestionTarget, skipIdea, agentQuestions, answerQuestions, agentsList, needsOf, settleNeeds, NEEDS_FOR, readLastWords, readNeeds, parseRead, READ_FILE, autoAllow, autoAllowSweep, allowlistInWork, installAllowlist, inWork, withScope, withTrust, sandboxFor, sandboxEnv, sandboxNeeds, sandboxState, sandboxWrites, SANDBOX_DIR, resumeFor, noteSession, trustFull, GUARD_SETTINGS, isUrgent, urgentFirst, urgentUndo, urgentDone, urgentSweep, urgentState , licenceGate };
|
package/browser.html
CHANGED
|
@@ -24,6 +24,7 @@ form.go{display:flex;gap:10px}
|
|
|
24
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
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
26
|
button.main{background:var(--bone);color:var(--bg);border-color:var(--bone)}
|
|
27
|
+
button:disabled{opacity:.6;cursor:progress}
|
|
27
28
|
button:focus-visible,input:focus-visible{outline:2px solid var(--amber);outline-offset:2px}
|
|
28
29
|
h2{margin:6px 0 0;font:600 12px var(--sans);letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}
|
|
29
30
|
.sites{display:grid;grid-template-columns:repeat(auto-fill,minmax(250px,1fr));gap:12px}
|
|
@@ -36,6 +37,7 @@ h2{margin:6px 0 0;font:600 12px var(--sans);letter-spacing:.14em;text-transform:
|
|
|
36
37
|
.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
38
|
.done p{margin:0;flex:1;min-width:220px;font-size:14px}
|
|
38
39
|
.msg{min-height:20px;font-size:14px;color:var(--amber)}
|
|
40
|
+
#donemsg{flex-basis:100%;min-height:0}#donemsg:empty{display:none}
|
|
39
41
|
</style>
|
|
40
42
|
</head>
|
|
41
43
|
<body>
|
|
@@ -54,13 +56,15 @@ h2{margin:6px 0 0;font:600 12px var(--sans);letter-spacing:.14em;text-transform:
|
|
|
54
56
|
<div class="done">
|
|
55
57
|
<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
58
|
<button class="main" id="done" type="button">Done</button>
|
|
59
|
+
<div class="msg" id="donemsg" role="status"></div>
|
|
57
60
|
</div>
|
|
58
61
|
</main>
|
|
59
62
|
<script>
|
|
60
63
|
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(); }); }
|
|
64
|
+
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) { if (!r.ok) throw new Error("Symbiot answered " + r.status); return r.json(); }); }
|
|
62
65
|
function esc(s) { return String(s == null ? "" : s).replace(/[&<>"']/g, function (c) { return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]; }); }
|
|
63
66
|
function say(t) { document.getElementById("msg").textContent = t || ""; }
|
|
67
|
+
function sayDone(t) { document.getElementById("donemsg").textContent = t || ""; } // beside Done, where you clicked
|
|
64
68
|
// a window of this same browser (so the sign-in is Symbiot's), the size of a laptop screen
|
|
65
69
|
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
70
|
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; }
|
|
@@ -76,7 +80,8 @@ function render(d) {
|
|
|
76
80
|
});
|
|
77
81
|
document.getElementById("list").innerHTML = h;
|
|
78
82
|
}
|
|
79
|
-
|
|
83
|
+
// hub=1: this page is still here (a reload isn't a close: server.mjs /api/browser/closed)
|
|
84
|
+
function load() { api("/api/links?hub=1").then(render).catch(function () { say("Symbiot isn't answering. Is it still running?"); }); }
|
|
80
85
|
document.getElementById("list").addEventListener("click", function (e) {
|
|
81
86
|
var b = e.target.closest && e.target.closest("button[data-id]"); if (!b) return;
|
|
82
87
|
var url = b.getAttribute("data-url"), id = b.getAttribute("data-id");
|
|
@@ -88,7 +93,20 @@ document.getElementById("list").addEventListener("click", function (e) {
|
|
|
88
93
|
});
|
|
89
94
|
// any site: opened, and kept under Your sites with whether you're signed in (links.mjs addSite)
|
|
90
95
|
document.getElementById("go").addEventListener("submit", function (e) { e.preventDefault(); var u = fromInput(document.getElementById("addr").value); if (!u) return; openSite(u); api("/api/links/site", { site: u, here: true }).then(function (r) { if (r && !r.error) { say("Sign in to it in the window that opened, then come back here. It's under " + ((r.item && r.item.group) || "Your sites") + " now."); load(); } }).catch(function () {}); });
|
|
91
|
-
|
|
96
|
+
// Done: Symbiot closes this browser (every window of it) and checks each site. Pressed, it
|
|
97
|
+
// says so at once and can't be pressed again meanwhile; if Symbiot can't close it, or isn't
|
|
98
|
+
// answering, it says that rather than nothing happening (2026-10-12).
|
|
99
|
+
var DONE = false, doneBtn = document.getElementById("done");
|
|
100
|
+
doneBtn.addEventListener("click", function () {
|
|
101
|
+
if (DONE) return; DONE = true; doneBtn.disabled = true; doneBtn.textContent = "Closing…";
|
|
102
|
+
sayDone("Closing, and checking your sites…");
|
|
103
|
+
api("/api/browser/done", {}).then(function (r) {
|
|
104
|
+
// still here: nothing of it was found to close, so this window closes itself
|
|
105
|
+
setTimeout(function () { window.close(); setTimeout(function () { DONE = false; doneBtn.disabled = false; doneBtn.textContent = "Done"; sayDone("Symbiot is checking your sites. Close this window with its X if it stays open."); }, 800); }, (r && r.closed) ? 3000 : 300);
|
|
106
|
+
}).catch(function () { DONE = false; doneBtn.disabled = false; doneBtn.textContent = "Done"; sayDone("Symbiot isn't answering, so it couldn't close this or check your sites. Is it still running? Then click Done again."); });
|
|
107
|
+
});
|
|
108
|
+
// Its X: Symbiot hears it as it goes, closes the rest of it and checks each site, as Done does.
|
|
109
|
+
window.addEventListener("pagehide", function () { if (!DONE && navigator.sendBeacon) navigator.sendBeacon("/api/browser/closed?t=" + encodeURIComponent(T), ""); });
|
|
92
110
|
load(); setInterval(load, 5000);
|
|
93
111
|
</script>
|
|
94
112
|
</body>
|