symbiot 0.58.2 → 0.58.3
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 +17 -0
- package/README.md +2 -0
- package/agents.mjs +67 -18
- package/core.mjs +29 -3
- package/guard.mjs +4 -3
- package/headless.mjs +105 -9
- package/home.mjs +8 -6
- package/index.mjs +60 -7
- package/lanes.mjs +9 -5
- package/marketing.mjs +86 -11
- package/mind.mjs +96 -20
- package/package.json +4 -3
- package/post.mjs +4 -2
- package/server.mjs +13 -3
- package/tray.mjs +154 -0
- package/ui.mjs +157 -32
- package/watch.mjs +57 -10
package/watch.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, statSyn
|
|
|
25
25
|
import { execFile } from "node:child_process";
|
|
26
26
|
import { randomBytes } from "node:crypto";
|
|
27
27
|
import { VERSION, CONFIG_DIR, loadConfig, saveConfig } from "./core.mjs";
|
|
28
|
-
import { readPage, siteUrl, isTrusted, signIn } from "./headless.mjs";
|
|
28
|
+
import { readPage, readLinkedIn, siteUrl, isTrusted, signIn } from "./headless.mjs";
|
|
29
29
|
import { loadScreens } from "./screens.mjs";
|
|
30
30
|
import { desktopNotify } from "./desktop.mjs";
|
|
31
31
|
import { resolveProvider, write } from "./ai.mjs";
|
|
@@ -39,6 +39,7 @@ const WATCH_FILE = join(CONFIG_DIR, "watch.json");
|
|
|
39
39
|
const EVERY = [5, 15, 30, 60]; // minutes between reads
|
|
40
40
|
// 40 watches: every standard Link (links.mjs) plus a company's own and yours; 1000 news: a
|
|
41
41
|
// busy inbox can't push the rest of the week out before Week is written
|
|
42
|
+
const BRIEF_HOURS = 72; // the longest window the Dashboard shows a card's items for
|
|
42
43
|
const MAX_WATCHES = 40, MAX_SEEN = 1000, MAX_NEWS = 1000, MAX_PER_READ = 25, MAX_BRIEFS = 50;
|
|
43
44
|
|
|
44
45
|
function loadWatch() {
|
|
@@ -191,15 +192,51 @@ function setBrief(on) { const cfg = loadConfig(); if (on) cfg.watchBrief = true;
|
|
|
191
192
|
// A chat list (chat: true) says in brackets who each chat's last message is from.
|
|
192
193
|
const CHAT_RULE = `This is a chat list (WhatsApp). A chat's line is its name and the start of its last message, and doesn't say who wrote that message: the brackets do. ` +
|
|
193
194
|
`Only a chat with unread messages from them can need the user. One whose last message is the user's own, or has nothing unread, never needs a reply: don't say it does, and never take its message as the contact's. Say "nothing needs you" when none is unread. `;
|
|
194
|
-
|
|
195
|
+
// LinkedIn (social: true): a message is the likeliest to need a reply, and the brief
|
|
196
|
+
// once skipped one listed under two notifications (2026-10-09). So every listed item
|
|
197
|
+
// gets a mention, messages first and on their own line.
|
|
198
|
+
const SOCIAL_RULE = `This is LinkedIn: messages and notifications. Mention every item listed, none left out (group alike notifications in one line if you need to). ` +
|
|
199
|
+
`Messages come first, on a line of their own starting "Messages:", naming who wrote and what they said when that's shown; an unread message usually wants a reply. Then the notifications, by who did what. `;
|
|
200
|
+
async function briefOf(news, name, { chat = false, social = false } = {}) {
|
|
195
201
|
if (!resolveProvider()) return "";
|
|
196
|
-
const system = `You triage what just arrived on a page someone watches: their email inbox, their chats, or their GitHub notifications. ` +
|
|
197
|
-
`In at most 3 short lines of plain text, first what needs them (and why, in a few words), then what can wait. ` + (chat ? CHAT_RULE : "") +
|
|
202
|
+
const system = `You triage what just arrived on a page someone watches: their email inbox, their chats, their LinkedIn or their GitHub notifications. ` +
|
|
203
|
+
`In at most ${social ? 4 : 3} short lines of plain text, first what needs them (and why, in a few words), then what can wait. ` + (chat ? CHAT_RULE : "") + (social ? SOCIAL_RULE : "") +
|
|
198
204
|
`Name things by sender and subject so they can find them. Use only what's shown; never guess at what a message says beyond it. No preamble, no markdown.`;
|
|
199
|
-
const
|
|
205
|
+
const line = (n) => (chat ? withFrom(n) : social && n.li ? `[${n.li === "message" ? "message" : n.type || "notification"}] ${n.text}` : social && /^Messaging\b/i.test(n.text) ? `[message] ${n.text}` : n.text);
|
|
206
|
+
const text = await write(system, `New on ${name}:\n${news.map((n) => "- " + line(n)).join("\n")}\n\nWhat needs me, and what can wait?`);
|
|
200
207
|
return text && !/^\(?couldn't reach the model/i.test(text) ? text.trim().slice(0, 600) : "";
|
|
201
208
|
}
|
|
202
209
|
|
|
210
|
+
// ---- LinkedIn: who and what, not only how many ----------------------------------
|
|
211
|
+
// Its top bar's badges ("Messaging, 1 new notification", "Notifications, 2 new
|
|
212
|
+
// notifications") were all Watch read there. A badge that's new is read out from
|
|
213
|
+
// LinkedIn's own lists (headless.mjs readLinkedIn): each unread conversation as
|
|
214
|
+
// "Message from <who>: <its last line>", each unread card in its own words, with
|
|
215
|
+
// who (actor) and what kind (type). None marked unread: the badge's count, newest
|
|
216
|
+
// first. A list that can't be read (the browser busy, signed out) keeps the badge.
|
|
217
|
+
const LI_BADGE = /^(Messaging|Notifications)\b[,:]?\s+(\d+)\s+new\b/i, LI_MSG = "https://www.linkedin.com/messaging/";
|
|
218
|
+
const LI_TYPES = [[/\bcommented\b/i, "comment"], [/\brepl(ied|y)\b/i, "reply"], [/\bmentioned\b/i, "mention"], [/\b(reacted|liked|celebrated|loves?|supports?|finds? .{0,40}(insightful|funny|curious))\b/i, "reaction"],
|
|
219
|
+
[/\b(reposted|shared)\b/i, "repost"], [/\b(viewed your profile|profile views?|appeared in \d+ searches)\b/i, "profile view"], [/\b(invit\w*|connection request|accepted your)\b/i, "invitation"],
|
|
220
|
+
[/\bendorsed\b/i, "endorsement"], [/\b(followed you|started following)\b/i, "follow"], [/\bjobs?\b/i, "job"], [/\b(birthday|work anniversary|new position|started a new)\b/i, "milestone"], [/\b(posted|published)\b/i, "post"]];
|
|
221
|
+
function liType(text) { for (const [re, t] of LI_TYPES) if (re.test(String(text || ""))) return t; return "notification"; }
|
|
222
|
+
const liPick = (list, n) => { const un = list.filter((x) => x.unread); return un.length ? un : list.slice(0, Math.max(1, n || 1)); };
|
|
223
|
+
// fresh (what's new this read) with its badges read out; look(kind) reads a list
|
|
224
|
+
async function linkedinDetail(fresh, look = readLinkedIn) {
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const it of fresh) {
|
|
227
|
+
const m = String(it.text || "").match(LI_BADGE); if (!m) { out.push(it); continue; }
|
|
228
|
+
const kind = /^messaging/i.test(m[1]) ? "messages" : "notifications";
|
|
229
|
+
let r = null; try { r = await look(kind); } catch {}
|
|
230
|
+
const got = r && Array.isArray(r.items) && r.items.length ? liPick(r.items, Number(m[2])) : [];
|
|
231
|
+
if (!got.length) { out.push(it); continue; }
|
|
232
|
+
for (const g of got) out.push(kind === "messages"
|
|
233
|
+
? { text: `Message from ${g.name}${g.preview ? ": " + g.preview : ""}`.slice(0, 300), href: LI_MSG, li: "message", sender: String(g.name).slice(0, 80) }
|
|
234
|
+
: { text: String(g.text).slice(0, 300), href: g.href || it.href, li: "notification", type: liType(g.text), ...(g.actor ? { actor: String(g.actor).slice(0, 80) } : {}) });
|
|
235
|
+
}
|
|
236
|
+
return out;
|
|
237
|
+
}
|
|
238
|
+
const liFields = (it) => (it.li ? { li: it.li, ...(it.sender ? { sender: it.sender } : {}), ...(it.actor ? { actor: it.actor } : {}), ...(it.type ? { type: it.type } : {}) } : {});
|
|
239
|
+
|
|
203
240
|
// ---- watches -------------------------------------------------------------------
|
|
204
241
|
// A watch whose last read found the sign-in page: it reads nothing until you sign in again.
|
|
205
242
|
const SIGNED_OUT = "Signed out of ";
|
|
@@ -669,7 +706,7 @@ async function openChat(id, { open = signIn } = {}) {
|
|
|
669
706
|
// briefOf (the tests pass their own). Gives the watch, plus `new` (what was new)
|
|
670
707
|
// and `brief`, or `learned` (the first read), or `busy` when the hidden browser
|
|
671
708
|
// is in use (nothing changes: it's read later).
|
|
672
|
-
async function checkWatch(id, { read = readPage, github = readGitHub, notify = desktopNotify, brief = briefOf } = {}) {
|
|
709
|
+
async function checkWatch(id, { read = readPage, github = readGitHub, notify = desktopNotify, brief = briefOf, linkedin = readLinkedIn, now = null } = {}) {
|
|
673
710
|
const w0 = loadWatch().watches.find((x) => x.id === id); if (!w0) return { error: `No watch ${id}.` };
|
|
674
711
|
const page = (isGitHubInbox(w0.url) && await github()) || (await read(w0.url)) || { error: "Nothing came back from the page." };
|
|
675
712
|
if (page.busy) return { ...view(w0), busy: true };
|
|
@@ -690,7 +727,13 @@ async function checkWatch(id, { read = readPage, github = readGitHub, notify = d
|
|
|
690
727
|
delete w.error;
|
|
691
728
|
// read another way than last time (gh, or the page while gh is out): the same
|
|
692
729
|
// things look different, so learn them again rather than announce them all
|
|
693
|
-
const first = !w.checked || (w.via || "") !== (page.via || ""),
|
|
730
|
+
const first = !w.checked || (w.via || "") !== (page.via || ""), seen0 = w.seen;
|
|
731
|
+
let { fresh, keys } = newItems(seen0, items);
|
|
732
|
+
// LinkedIn: a new badge, read out into who and what (and those remembered too, so a message still unread isn't news twice)
|
|
733
|
+
if (!first && isSocial(w.url) && fresh.some((it) => LI_BADGE.test(String(it.text || "")))) {
|
|
734
|
+
const told = newItems(seen0, await linkedinDetail(fresh, linkedin));
|
|
735
|
+
fresh = told.fresh; keys = [...keys, ...told.keys.filter((k) => !keys.includes(k))];
|
|
736
|
+
}
|
|
694
737
|
if (page.via) w.via = page.via; else delete w.via;
|
|
695
738
|
w.seen = remember(w.seen, keys); w.checked = w.last;
|
|
696
739
|
const chat = isChat(w.url);
|
|
@@ -703,7 +746,7 @@ async function checkWatch(id, { read = readPage, github = readGitHub, notify = d
|
|
|
703
746
|
}
|
|
704
747
|
if (chat) recheckChats(d.news.filter((n) => n.watch === w.id), items);
|
|
705
748
|
const news = first ? [] : fresh.slice(0, MAX_PER_READ).map((it) => ({ id: randomBytes(4).toString("hex"), watch: w.id, name: w.name, ts: w.last, text: it.text.slice(0, 300), ...(it.href ? { href: it.href } : {}),
|
|
706
|
-
...(chat ? { from: fromOf(it), ...(it.unread ? { unread: it.unread } : {}) } : {}), ...(readMark && !it.unread ? { read: true } : {}) }));
|
|
749
|
+
...(chat ? { from: fromOf(it), ...(it.unread ? { unread: it.unread } : {}) } : {}), ...(readMark && !it.unread ? { read: true } : {}), ...liFields(it) }));
|
|
707
750
|
d.news = [...news, ...d.news].slice(0, MAX_NEWS);
|
|
708
751
|
saveWatch(d);
|
|
709
752
|
if (first) return { ...view(w), learned: keys.length };
|
|
@@ -711,7 +754,11 @@ async function checkWatch(id, { read = readPage, github = readGitHub, notify = d
|
|
|
711
754
|
const needy = news.filter((n) => needsYou(n, w));
|
|
712
755
|
let said = "";
|
|
713
756
|
if (needy.length && briefOn()) {
|
|
714
|
-
|
|
757
|
+
// the brief covers what the card lists (BRIEF_HOURS), not only this read's: the card
|
|
758
|
+
// showed a message from two days before, and the brief left it out (2026-10-09)
|
|
759
|
+
const social = isSocial(w.url), since = (now || w.last) - BRIEF_HOURS * 3600000;
|
|
760
|
+
const listed = social ? d.news.filter((n) => n.watch === w.id && n.ts >= since && unseen(n, w) && needsYou(n, w)).slice(0, MAX_PER_READ) : null;
|
|
761
|
+
said = await brief(chat ? news : listed || needy, w.name, { chat, social }).catch(() => ""); // a chat's brief says who each is from; mail you've read isn't news
|
|
715
762
|
if (said) { const d2 = loadWatch(); d2.briefs = [{ id: randomBytes(4).toString("hex"), watch: w.id, name: w.name, ts: w.last, count: needy.length, text: said }, ...d2.briefs].slice(0, MAX_BRIEFS); saveWatch(d2); }
|
|
716
763
|
}
|
|
717
764
|
if (needy.length) notify(...newsNotice(needy, w.name, said));
|
|
@@ -734,4 +781,4 @@ function startWatches(opts = {}) {
|
|
|
734
781
|
return () => { clearTimeout(first); clearInterval(every); };
|
|
735
782
|
}
|
|
736
783
|
|
|
737
|
-
export { replyOf, draftCard, draftCards, draftAnswer, isDraftDir, sendBrief, LINK_ASK, WATCH_FILE, EVERY, GITHUB_INBOX, DRAFTS_DIR, itemsOf, fromOf, chatName, recheckChats, itemKey, newItems, remember, isGitHubInbox, githubItems, readGitHub, setBrief, briefOf, newsNotice, markNews, watchState, addWatch, setEvery, removeWatch, clearNews, seenWatch, newsSince, newsAfter, waitingOn, watchBoard, boardLine, boardChat, boardTalk, clearBoardChat, talkOf, isSignedOut, isMail, isChat, isSocial, draftsUrl, draftBrief, chatBrief, socialBrief, draftReply, openChat, checkWatch, dueWatches, startWatches };
|
|
784
|
+
export { linkedinDetail, liType, replyOf, draftCard, draftCards, draftAnswer, isDraftDir, sendBrief, LINK_ASK, WATCH_FILE, EVERY, GITHUB_INBOX, DRAFTS_DIR, itemsOf, fromOf, chatName, recheckChats, itemKey, newItems, remember, isGitHubInbox, githubItems, readGitHub, setBrief, briefOf, newsNotice, markNews, watchState, addWatch, setEvery, removeWatch, clearNews, seenWatch, newsSince, newsAfter, waitingOn, watchBoard, boardLine, boardChat, boardTalk, clearBoardChat, talkOf, isSignedOut, isMail, isChat, isSocial, draftsUrl, draftBrief, chatBrief, socialBrief, draftReply, openChat, checkWatch, dueWatches, startWatches };
|