symbiot 0.61.0 → 0.62.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 +25 -0
- package/README.md +80 -34
- package/browser.html +2 -1
- package/core.mjs +3 -1
- package/guard.mjs +30 -1
- package/handover.mjs +1 -1
- package/headless.mjs +83 -8
- package/home.mjs +48 -12
- package/index.mjs +31 -17
- package/links.mjs +40 -5
- package/mdns.mjs +135 -0
- package/mind.mjs +1 -0
- package/package.json +5 -3
- package/phone.mjs +525 -78
- package/post.mjs +29 -12
- package/qr.mjs +160 -0
- package/server.mjs +26 -13
- package/tasks.mjs +12 -2
- package/ui.mjs +173 -35
package/post.mjs
CHANGED
|
@@ -33,7 +33,7 @@ import { resolveProvider, write } from "./ai.mjs";
|
|
|
33
33
|
import { commits, discoveredRepos, laneMap } from "./scan.mjs";
|
|
34
34
|
import { readTexts, pagePicture, pageClip, siteUrl, CLIP } from "./headless.mjs";
|
|
35
35
|
import { runHandoff, runningHandoff } from "./agents.mjs";
|
|
36
|
-
import { MARKETING_DIR, ensureMarketing, setDraftStatus, productOf, productNames, draftStatuses, postedCmd } from "./marketing.mjs";
|
|
36
|
+
import { MARKETING_DIR, ensureMarketing, setDraftStatus, productOf, productNames, draftStatuses, postedCmd, parseDraft, inLane } from "./marketing.mjs";
|
|
37
37
|
|
|
38
38
|
const PATHS = { posts: join(CONFIG_DIR, "posts.json"), log: join(CONFIG_DIR, "posts-log.jsonl"), voice: join(CONFIG_DIR, "voice.md"), media: join(CONFIG_DIR, "post-media"), test: join(CONFIG_DIR, "post-test.json") };
|
|
39
39
|
const PLATFORM = "linkedin";
|
|
@@ -559,21 +559,38 @@ const CUSTOMER = [
|
|
|
559
559
|
// first draft; week 1 is the first of them a post went out in (until one has, the one
|
|
560
560
|
// you're in: a week with nothing posted doesn't count), or the day post-test.json's
|
|
561
561
|
// start names, when you've moved it ({ "start": "2026-10-13" }). Each row: posts published, and how many
|
|
562
|
-
// carried a picture or video
|
|
563
|
-
//
|
|
562
|
+
// carried a picture or video (approved under Drafts to post, or gone out through Marketing:
|
|
563
|
+
// laneOut); replies on LinkedIn (its watched notifications, marked
|
|
564
|
+
// by watch.mjs markNews: comments, replies, mentions and messages, not reactions or a
|
|
565
|
+
// badge's bare count), the ones that may be a customer and those asking what it
|
|
564
566
|
// costs; and npm installs of the packages your posts are about. Profile visits are
|
|
565
567
|
// only in LinkedIn's own analytics. null before there's a first draft.
|
|
566
568
|
const TEST_WEEKS = 4;
|
|
569
|
+
const isReply = (n) => !!n.social && (n.li === "message" || ["comment", "reply", "mention"].includes(n.type) || !!n.customer);
|
|
567
570
|
const dayStart = (ts) => { const d = new Date(ts); d.setHours(0, 0, 0, 0); return d.getTime(); };
|
|
568
571
|
const addDays = (ts, n) => { const d = new Date(ts); d.setDate(d.getDate() + n); return d.getTime(); };
|
|
569
572
|
const ymd = (ts) => { const d = new Date(ts); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`; };
|
|
570
|
-
|
|
573
|
+
// What went out through Marketing's lane: drafts its agent marked posted (when they went
|
|
574
|
+
// out), or approved for a time in the platform's scheduler that has come. Not one handed
|
|
575
|
+
// over from Drafts to post (post-<id>.md): the log has that one, from its approve. [{ ts, media }]
|
|
576
|
+
function laneOut(dir = MARKETING_DIR, now = Date.now()) {
|
|
577
|
+
const out = [];
|
|
578
|
+
for (const [rel, s] of Object.entries(draftStatuses(dir))) {
|
|
579
|
+
if (!s || /(^|\/)post-[0-9a-f]{8}\.md$/i.test(rel)) continue;
|
|
580
|
+
const ts = Number(s.posted) || (s.status === "approved" && s.scheduled ? Date.parse(String(s.scheduled).replace(" ", "T")) : NaN);
|
|
581
|
+
if (!Number.isFinite(ts) || ts > now) continue;
|
|
582
|
+
let media = 0; try { media = parseDraft(readFileSync(inLane(dir, rel), "utf8")).media.length; } catch {} // its file gone: still out
|
|
583
|
+
out.push({ ts, media });
|
|
584
|
+
}
|
|
585
|
+
return out;
|
|
586
|
+
}
|
|
587
|
+
function testPlan(paths = PATHS, now = Date.now(), lane = []) {
|
|
571
588
|
const posts = loadPosts(paths).posts;
|
|
572
589
|
let set = NaN; try { set = Date.parse(String(JSON.parse(readFileSync(paths.test, "utf8")).start || "").slice(0, 10) + "T00:00:00"); } catch {}
|
|
573
|
-
const first = posts.reduce((m, p) => Math.min(m, Number(p.drafted) || Infinity), Infinity);
|
|
590
|
+
const first = Math.min(posts.reduce((m, p) => Math.min(m, Number(p.drafted) || Infinity), Infinity), ...lane.map((l) => l.ts));
|
|
574
591
|
if (!set && first === Infinity) return null;
|
|
575
592
|
let start = set ? dayStart(set) : dayStart(first);
|
|
576
|
-
if (!set) { const out = postLog(paths).filter((l) => l.action === "approved").map((l) => Number(l.ts)).sort((a, b) => a - b)[0], to = out || now; while (addDays(start, 7) <= to) start = addDays(start, 7); }
|
|
593
|
+
if (!set) { const out = [...postLog(paths).filter((l) => l.action === "approved").map((l) => Number(l.ts)), ...lane.map((l) => l.ts)].sort((a, b) => a - b)[0], to = out || now; while (addDays(start, 7) <= to) start = addDays(start, 7); }
|
|
577
594
|
return { start, weeks: Array.from({ length: TEST_WEEKS }, (_, i) => [addDays(start, 7 * i), addDays(start, 7 * (i + 1))]), posts };
|
|
578
595
|
}
|
|
579
596
|
// The npm packages your posts are about: the repos their git facts came from
|
|
@@ -584,12 +601,12 @@ function testPackages(posts, map = {}) {
|
|
|
584
601
|
for (const r of repos) { if (!map[r]) continue; try { const j = JSON.parse(readFileSync(join(map[r], "package.json"), "utf8")); if (j.name && !j.private && !out.includes(j.name)) out.push(j.name); } catch {} }
|
|
585
602
|
return out.slice(0, 3);
|
|
586
603
|
}
|
|
587
|
-
function testWeeks({ paths = PATHS, now = Date.now(), news = [], installs = null, map = {} } = {}) {
|
|
588
|
-
const plan = testPlan(paths, now); if (!plan) return null;
|
|
589
|
-
const
|
|
604
|
+
function testWeeks({ paths = PATHS, now = Date.now(), news = [], installs = null, map = {}, dir = MARKETING_DIR } = {}) {
|
|
605
|
+
const lane = laneOut(dir, now), plan = testPlan(paths, now, lane); if (!plan) return null;
|
|
606
|
+
const out = [...postLog(paths).filter((l) => l.action === "approved").map((l) => ({ ts: Number(l.ts), media: (l.media || []).length })), ...lane];
|
|
590
607
|
const rows = plan.weeks.map(([from, to], i) => {
|
|
591
|
-
const inW = (t) => Number(t) >= from && Number(t) < to, pub =
|
|
592
|
-
return { week: i + 1, from: ymd(from), to: ymd(addDays(to, -1)), started: now >= from, over: now >= to, published: pub.length, media: pub.filter((
|
|
608
|
+
const inW = (t) => Number(t) >= from && Number(t) < to, pub = out.filter((o) => inW(o.ts)), rep = news.filter((n) => isReply(n) && inW(n.ts));
|
|
609
|
+
return { week: i + 1, from: ymd(from), to: ymd(addDays(to, -1)), started: now >= from, over: now >= to, published: pub.length, media: pub.filter((o) => o.media).length,
|
|
593
610
|
replies: rep.length, customers: rep.filter((n) => n.customer).length, pricing: rep.filter((n) => n.customer === "pricing").length, installs: installs && installs[i] != null ? installs[i] : null };
|
|
594
611
|
});
|
|
595
612
|
return { start: ymd(plan.start), end: ymd(addDays(plan.weeks[TEST_WEEKS - 1][1], -1)), rows, packages: testPackages(plan.posts, map), week: rows.filter((r) => r.started).length };
|
|
@@ -613,5 +630,5 @@ async function testInstalls(t, { now = Date.now(), get = null } = {}) {
|
|
|
613
630
|
}
|
|
614
631
|
function maybeCustomer(text) { const s = String(text || ""); const hit = CUSTOMER.find(([, re]) => re.test(s)); return hit ? hit[0] : ""; }
|
|
615
632
|
|
|
616
|
-
export { TEST_WEEKS, testWeeks, testInstalls, testPackages, PATHS, PLATFORM, SHARE_URL, KINDS, KIND_LABEL, LINKEDIN_ACTIVITY, NO_AI, voiceOf, loadVoice, voiceFromLinkedIn, tagsIn, changelogIn, gatherFacts, factLine, numbersIn, namesIn, checkClaims, parseDrafts, vetDraft, draftPosts, loadPosts, postLog, postsState, editPost, skipPost, approvePost, handToMarketing, copyText, openUrl, maybeCustomer,
|
|
633
|
+
export { TEST_WEEKS, testWeeks, testInstalls, testPackages, laneOut, PATHS, PLATFORM, SHARE_URL, KINDS, KIND_LABEL, LINKEDIN_ACTIVITY, NO_AI, voiceOf, loadVoice, voiceFromLinkedIn, tagsIn, changelogIn, gatherFacts, factLine, numbersIn, namesIn, checkClaims, parseDrafts, vetDraft, draftPosts, loadPosts, postLog, postsState, editPost, skipPost, approvePost, handToMarketing, copyText, openUrl, maybeCustomer,
|
|
617
634
|
mediaType, mediaDir, mediaWords, addMedia, removeMedia, mediaFile, pictureOfPage, clipOfPage, concatList, toVideo, canClip, NO_FFMPEG, localApps, screenFor, picturesFor, listening };
|
package/qr.mjs
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// symbiot — a QR code for pairing your phone (phone.mjs): byte mode, error
|
|
2
|
+
// correction level M, versions 1 to 20 (up to 666 bytes), with no dependency.
|
|
3
|
+
// qrMatrix(text) gives the modules (true is dark); qrSvg(text) an SVG of them with
|
|
4
|
+
// the quiet zone around, sized by the viewBox so the page scales it.
|
|
5
|
+
// ISO/IEC 18004: the layout tables below are its level M rows.
|
|
6
|
+
|
|
7
|
+
// [EC codewords per block, blocks in group 1, data codewords each, blocks in group 2, data codewords each]
|
|
8
|
+
const M_BLOCKS = [null,
|
|
9
|
+
[10, 1, 16, 0, 0], [16, 1, 28, 0, 0], [26, 1, 44, 0, 0], [18, 2, 32, 0, 0], [24, 2, 43, 0, 0],
|
|
10
|
+
[16, 4, 27, 0, 0], [18, 4, 31, 0, 0], [22, 2, 38, 2, 39], [22, 3, 36, 2, 37], [26, 4, 43, 1, 44],
|
|
11
|
+
[30, 1, 50, 4, 51], [22, 6, 36, 2, 37], [22, 8, 37, 1, 38], [24, 4, 40, 5, 41], [24, 5, 41, 5, 42],
|
|
12
|
+
[28, 7, 45, 3, 46], [28, 10, 46, 1, 47], [26, 9, 43, 4, 44], [26, 3, 44, 11, 45], [26, 3, 41, 13, 42]];
|
|
13
|
+
const ALIGN = [null, [], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50],
|
|
14
|
+
[6, 30, 54], [6, 32, 58], [6, 34, 62], [6, 26, 46, 66], [6, 26, 48, 70], [6, 26, 50, 74], [6, 30, 54, 78], [6, 30, 56, 82], [6, 30, 58, 86], [6, 34, 62, 90]];
|
|
15
|
+
const REMAINDER = (v) => (v >= 2 && v <= 6 ? 7 : v >= 14 && v <= 20 ? 3 : 0);
|
|
16
|
+
const MAX_VERSION = 20;
|
|
17
|
+
const dataCodewords = (v) => { const [, b1, d1, b2, d2] = M_BLOCKS[v]; return b1 * d1 + b2 * d2; };
|
|
18
|
+
const countBits = (v) => (v < 10 ? 8 : 16);
|
|
19
|
+
|
|
20
|
+
// ---- Reed-Solomon over GF(256), polynomial 0x11D --------------------------------
|
|
21
|
+
const EXP = new Uint8Array(512), LOG = new Uint8Array(256);
|
|
22
|
+
for (let i = 0, x = 1; i < 255; i++) { EXP[i] = x; LOG[x] = i; x <<= 1; if (x & 0x100) x ^= 0x11d; }
|
|
23
|
+
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255];
|
|
24
|
+
const mul = (a, b) => (a && b ? EXP[LOG[a] + LOG[b]] : 0);
|
|
25
|
+
function generator(n) {
|
|
26
|
+
let g = [1];
|
|
27
|
+
for (let i = 0; i < n; i++) { const next = new Array(g.length + 1).fill(0); g.forEach((c, j) => { next[j] ^= c; next[j + 1] ^= mul(c, EXP[i]); }); g = next; }
|
|
28
|
+
return g;
|
|
29
|
+
}
|
|
30
|
+
function ecFor(data, n) {
|
|
31
|
+
const g = generator(n), r = [...data, ...new Array(n).fill(0)];
|
|
32
|
+
for (let i = 0; i < data.length; i++) { const f = r[i]; if (f) for (let j = 0; j < g.length; j++) r[i + j] ^= mul(g[j], f); }
|
|
33
|
+
return r.slice(data.length);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---- the codewords ---------------------------------------------------------------
|
|
37
|
+
function versionFor(bytes) {
|
|
38
|
+
for (let v = 1; v <= MAX_VERSION; v++) if (4 + countBits(v) + bytes.length * 8 <= dataCodewords(v) * 8) return v;
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
function codewords(bytes, v) {
|
|
42
|
+
const bits = [], put = (val, n) => { for (let i = n - 1; i >= 0; i--) bits.push((val >>> i) & 1); };
|
|
43
|
+
put(4, 4); put(bytes.length, countBits(v)); for (const b of bytes) put(b, 8);
|
|
44
|
+
const cap = dataCodewords(v) * 8;
|
|
45
|
+
put(0, Math.min(4, cap - bits.length));
|
|
46
|
+
while (bits.length % 8) bits.push(0);
|
|
47
|
+
const data = [];
|
|
48
|
+
for (let i = 0; i < bits.length; i += 8) data.push(bits.slice(i, i + 8).reduce((a, b) => (a << 1) | b, 0));
|
|
49
|
+
for (let pad = 0; data.length < cap / 8; pad ^= 1) data.push(pad ? 0x11 : 0xec);
|
|
50
|
+
// split into blocks, each with its EC codewords; then interleave column by column
|
|
51
|
+
const [ec, b1, d1, b2, d2] = M_BLOCKS[v], blocks = [];
|
|
52
|
+
let at = 0;
|
|
53
|
+
for (let i = 0; i < b1 + b2; i++) { const n = i < b1 ? d1 : d2, d = data.slice(at, at + n); at += n; blocks.push({ d, e: ecFor(d, ec) }); }
|
|
54
|
+
const out = [], longest = Math.max(d1, d2);
|
|
55
|
+
for (let i = 0; i < longest; i++) for (const b of blocks) if (i < b.d.length) out.push(b.d[i]);
|
|
56
|
+
for (let i = 0; i < ec; i++) for (const b of blocks) out.push(b.e[i]);
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---- the matrix ------------------------------------------------------------------
|
|
61
|
+
// Function patterns (finders, timing, alignment, format and version areas) are
|
|
62
|
+
// marked in `fixed`, so the data and the masks go around them.
|
|
63
|
+
function frame(v) {
|
|
64
|
+
const n = 17 + 4 * v, m = Array.from({ length: n }, () => new Array(n).fill(false)), fixed = Array.from({ length: n }, () => new Array(n).fill(false));
|
|
65
|
+
const set = (r, c, dark) => { if (r >= 0 && r < n && c >= 0 && c < n) { m[r][c] = dark; fixed[r][c] = true; } };
|
|
66
|
+
for (const [r0, c0] of [[0, 0], [0, n - 7], [n - 7, 0]])
|
|
67
|
+
for (let r = -1; r <= 7; r++) for (let c = -1; c <= 7; c++) {
|
|
68
|
+
const ring = Math.max(Math.abs(r - 3), Math.abs(c - 3));
|
|
69
|
+
set(r0 + r, c0 + c, ring !== 2 && ring !== 4 && r >= 0 && r <= 6 && c >= 0 && c <= 6);
|
|
70
|
+
}
|
|
71
|
+
for (let i = 8; i < n - 8; i++) { set(6, i, i % 2 === 0); set(i, 6, i % 2 === 0); }
|
|
72
|
+
const pos = ALIGN[v];
|
|
73
|
+
for (const r of pos) for (const c of pos) {
|
|
74
|
+
if ((r === 6 && c === 6) || (r === 6 && c === n - 7) || (r === n - 7 && c === 6)) continue;
|
|
75
|
+
for (let dr = -2; dr <= 2; dr++) for (let dc = -2; dc <= 2; dc++) set(r + dr, c + dc, Math.max(Math.abs(dr), Math.abs(dc)) !== 1);
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < 9; i++) { if (!fixed[8][i]) set(8, i, false); if (!fixed[i][8]) set(i, 8, false); }
|
|
78
|
+
for (let i = 0; i < 8; i++) { set(8, n - 1 - i, false); set(n - 1 - i, 8, false); }
|
|
79
|
+
set(n - 8, 8, true); // the dark module, beside the bottom-left format bits
|
|
80
|
+
if (v >= 7) for (let i = 0; i < 6; i++) for (let j = 0; j < 3; j++) { set(i, n - 11 + j, false); set(n - 11 + j, i, false); }
|
|
81
|
+
return { n, m, fixed };
|
|
82
|
+
}
|
|
83
|
+
// The data modules in placement order: two columns at a time from the right,
|
|
84
|
+
// up then down, skipping the vertical timing column.
|
|
85
|
+
function dataOrder(n, fixed) {
|
|
86
|
+
const out = [];
|
|
87
|
+
for (let right = n - 1, up = true; right > 0; right -= 2, up = !up) {
|
|
88
|
+
if (right === 6) right = 5;
|
|
89
|
+
for (let i = 0; i < n; i++) { const r = up ? n - 1 - i : i; for (const c of [right, right - 1]) if (!fixed[r][c]) out.push([r, c]); }
|
|
90
|
+
}
|
|
91
|
+
return out;
|
|
92
|
+
}
|
|
93
|
+
const MASKS = [(r, c) => (r + c) % 2 === 0, (r) => r % 2 === 0, (r, c) => c % 3 === 0, (r, c) => (r + c) % 3 === 0,
|
|
94
|
+
(r, c) => (Math.floor(r / 2) + Math.floor(c / 3)) % 2 === 0, (r, c) => ((r * c) % 2) + ((r * c) % 3) === 0,
|
|
95
|
+
(r, c) => (((r * c) % 2) + ((r * c) % 3)) % 2 === 0, (r, c) => (((r + c) % 2) + ((r * c) % 3)) % 2 === 0];
|
|
96
|
+
const bch = (val, gen, genBits) => { let v = val << (genBits - 1); for (let i = 31 - Math.clz32(v); i >= genBits - 1; i--) if ((v >>> i) & 1) v ^= gen << (i - genBits + 1); return v; };
|
|
97
|
+
// level M is 00; the 15 bits go around the top-left finder and split between the other two
|
|
98
|
+
function formatBits(mask) { const d = (0 << 3) | mask; return ((d << 10) | bch(d, 0x537, 11)) ^ 0x5412; }
|
|
99
|
+
function placeFormat(m, n, mask) {
|
|
100
|
+
const f = formatBits(mask), bit = (i) => ((f >>> i) & 1) === 1;
|
|
101
|
+
for (let i = 0; i < 15; i++) {
|
|
102
|
+
const b = bit(i);
|
|
103
|
+
// around the top-left finder
|
|
104
|
+
if (i < 6) m[i][8] = b; else if (i < 8) m[i + 1][8] = b; else if (i === 8) m[8][7] = b; else m[8][14 - i] = b;
|
|
105
|
+
// the copy, split between the top-right and bottom-left finders
|
|
106
|
+
if (i < 8) m[8][n - 1 - i] = b; else m[n - 15 + i][8] = b;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function placeVersion(m, n, v) {
|
|
110
|
+
if (v < 7) return;
|
|
111
|
+
const bits = (v << 12) | bch(v, 0x1f25, 13);
|
|
112
|
+
for (let i = 0; i < 18; i++) { const b = ((bits >>> i) & 1) === 1, r = Math.floor(i / 3), c = n - 11 + (i % 3); m[r][c] = b; m[c][r] = b; }
|
|
113
|
+
}
|
|
114
|
+
function penalty(m, n) {
|
|
115
|
+
let p = 0;
|
|
116
|
+
for (let pass = 0; pass < 2; pass++) for (let i = 0; i < n; i++) {
|
|
117
|
+
let run = 1;
|
|
118
|
+
for (let j = 1; j <= n; j++) {
|
|
119
|
+
const a = j < n ? (pass ? m[j][i] : m[i][j]) : null, b = pass ? m[j - 1][i] : m[i][j - 1];
|
|
120
|
+
if (a === b) run++; else { if (run >= 5) p += run - 2; run = 1; }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
for (let r = 0; r < n - 1; r++) for (let c = 0; c < n - 1; c++) { const v = m[r][c]; if (v === m[r][c + 1] && v === m[r + 1][c] && v === m[r + 1][c + 1]) p += 3; }
|
|
124
|
+
const pat = [true, false, true, true, true, false, true], at = (r, c, horiz) => (horiz ? m[r][c] : m[c][r]);
|
|
125
|
+
for (let pass = 0; pass < 2; pass++) for (let i = 0; i < n; i++) for (let j = 0; j + 7 <= n; j++) {
|
|
126
|
+
if (!pat.every((x, k) => at(i, j + k, !pass) === x)) continue;
|
|
127
|
+
const light = (from) => { for (let k = from; k < from + 4; k++) if (k >= 0 && k < n && at(i, k, !pass)) return false; return true; };
|
|
128
|
+
if (light(j - 4)) p += 40;
|
|
129
|
+
if (light(j + 7)) p += 40;
|
|
130
|
+
}
|
|
131
|
+
let dark = 0; for (const row of m) for (const x of row) if (x) dark++;
|
|
132
|
+
p += Math.floor(Math.abs((dark * 100) / (n * n) - 50) / 5) * 10;
|
|
133
|
+
return p;
|
|
134
|
+
}
|
|
135
|
+
function qrMatrix(text, { mask = -1 } = {}) {
|
|
136
|
+
const bytes = [...Buffer.from(String(text), "utf8")], v = versionFor(bytes);
|
|
137
|
+
if (!v) throw new Error("too long for a QR code here");
|
|
138
|
+
const words = codewords(bytes, v), { n, m, fixed } = frame(v), order = dataOrder(n, fixed);
|
|
139
|
+
const bits = []; for (const w of words) for (let i = 7; i >= 0; i--) bits.push((w >>> i) & 1);
|
|
140
|
+
for (let i = 0; i < REMAINDER(v); i++) bits.push(0);
|
|
141
|
+
let best = null;
|
|
142
|
+
for (let k = 0; k < 8; k++) {
|
|
143
|
+
if (mask >= 0 && k !== mask) continue;
|
|
144
|
+
const t = m.map((row) => row.slice());
|
|
145
|
+
order.forEach(([r, c], i) => { t[r][c] = (bits[i] === 1) !== MASKS[k](r, c); });
|
|
146
|
+
placeFormat(t, n, k); placeVersion(t, n, v);
|
|
147
|
+
const score = mask >= 0 ? 0 : penalty(t, n);
|
|
148
|
+
if (!best || score < best.score) best = { score, t, k };
|
|
149
|
+
}
|
|
150
|
+
return { version: v, mask: best.k, size: n, modules: best.t };
|
|
151
|
+
}
|
|
152
|
+
// An SVG of it, black on white with the 4-module quiet zone a camera needs.
|
|
153
|
+
function qrSvg(text) {
|
|
154
|
+
const { size, modules } = qrMatrix(text), q = 4, all = size + 2 * q;
|
|
155
|
+
let d = "";
|
|
156
|
+
modules.forEach((row, r) => row.forEach((dark, c) => { if (dark) d += `M${c + q} ${r + q}h1v1h-1z`; }));
|
|
157
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${all} ${all}" shape-rendering="crispEdges"><rect width="${all}" height="${all}" fill="#fff"/><path d="${d}" fill="#000"/></svg>`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export { qrMatrix, qrSvg, M_BLOCKS, ALIGN, REMAINDER, frame, dataOrder, MASKS, countBits };
|
package/server.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// `symbiot app`: the local server behind the app window. It serves the page
|
|
2
2
|
// (ui.mjs) and its /api routes, each a thin call into the module that does the
|
|
3
|
-
// work, on 127.0.0.1 only and behind the per-install token. (
|
|
3
|
+
// work, on 127.0.0.1 only and behind the per-install token. (Your phone's link
|
|
4
4
|
// listens on your network separately, only for that: phone.mjs.)
|
|
5
5
|
import { languageState, setLanguage, translate, allTranslations } from "./lang.mjs";
|
|
6
6
|
import { licenceState, setKey, clearKey, can, refreshRevoked, FREE } from "./licence.mjs";
|
|
@@ -25,20 +25,20 @@ 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, openSymbiotBrowser, closeSymbiotBrowser, setBrowserHub, siteUrl } from "./headless.mjs";
|
|
28
|
+
import { mapPage, wholePage, pressRegion, typeRegion, uploadRegion, scrollPage, signIn, keepBrowserOpen, isTrusted, trustedSites, trustSite, untrustSite, openSymbiotBrowser, closeSymbiotBrowser, setBrowserHub, siteUrl, browserHub } 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
|
-
import { linksState, linkSite, checkLink, unlinkSite } from "./links.mjs";
|
|
31
|
+
import { linksState, linkSite, checkLink, unlinkSite, addSite, signInAsked } from "./links.mjs";
|
|
32
32
|
import { testWeeks, testInstalls, postsState, draftPosts, approvePost, editPost, skipPost, voiceFromLinkedIn, addMedia, removeMedia, mediaFile, mediaDir, pictureOfPage, clipOfPage, openUrl } from "./post.mjs";
|
|
33
33
|
import { mindState, forget } from "./mind.mjs";
|
|
34
34
|
import { lanesTick, lanesState, partlyDone, orcaRelink } from "./lanes.mjs";
|
|
35
35
|
import { keepFacts, skipFacts, awaitTick, awaitingState, stopWaiting } from "./handback.mjs";
|
|
36
36
|
import { adaptState, noteUse } from "./adapt.mjs";
|
|
37
|
-
import { reportIdeasAdd, reportAsk, reportDraftAnswer, homeState, homeAsk, homeAnswer, homeNext, workScene, workGo, workTick, firstSteps, marketingState, marketingGo, marketingDraftAnswer, marketingTask, moveToMarketing, onboarding, setOnboarding } from "./home.mjs";
|
|
37
|
+
import { reportIdeasAdd, reportAsk, reportDraftAnswer, homeState, homeAsk, homeAnswer, homeNext, workScene, workGo, workTick, firstSteps, marketingState, marketingGo, marketingDraftAnswer, marketingTask, moveToMarketing, onboarding, setOnboarding, startOnboarding } from "./home.mjs";
|
|
38
38
|
import { MARKETING_DIR, MARKETING, draftPreview, laneMedia, displayName, setDraftMedia } from "./marketing.mjs";
|
|
39
39
|
import { trays, trayMedia, setBlur, renderCapture } from "./tray.mjs";
|
|
40
40
|
import { listReports, readReport, reportImage, markAllRead } from "./reports.mjs";
|
|
41
|
-
import { phoneState, setPhoneLink, newCode, unpairPhone, pairComputer, forgetComputer, pollComputer, startPhone } from "./phone.mjs";
|
|
41
|
+
import { phoneState, setPhoneLink, newCode, unpairPhone, setRelay, findComputers, pairComputer, forgetComputer, pollComputer, queueChange, dismissNote, setPhoneApprove, startPhone } from "./phone.mjs";
|
|
42
42
|
import { knowledgeState, addKnowledgeFolder, removeKnowledgeFolder, indexKnowledge, knowledgeTick, searchKnowledge } from "./knowledge.mjs";
|
|
43
43
|
import { runChecks, checksState, markClashesSeen } from "./checks.mjs";
|
|
44
44
|
|
|
@@ -240,7 +240,8 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
240
240
|
// an installed Symbiot keeps its app-menu entry pointing at itself (a new Node, a moved
|
|
241
241
|
// install); not a copy run from a checkout or a test
|
|
242
242
|
if (/[\\/]node_modules[\\/]symbiot[\\/]/.test(bin || "") && !SANDBOX && !process.env.SYMBIOT_NO_LAUNCHER) { try { installLauncher({ script: bin }); } catch {} }
|
|
243
|
-
|
|
243
|
+
// setup's first step: "Your computer" in the Android app, so a phone paired by its QR shows it
|
|
244
|
+
if (FRESH) { try { startOnboarding(); } catch {} }
|
|
244
245
|
// your projects: kept, and found again off the main thread, so nothing waits on a search
|
|
245
246
|
setScanOptions({ cache: true }); REPO_STATE.onChange = () => { try { homeState({ fresh: true }); } catch {} };
|
|
246
247
|
if (!SANDBOX) { refreshRevoked().catch(() => {}); setInterval(() => refreshRevoked().catch(() => {}), 6 * 3600 * 1000).unref(); } // cancelled Pro keys, about daily
|
|
@@ -382,8 +383,10 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
382
383
|
// Links (links.mjs): one click per standard work site: sign in, trust it, watch it.
|
|
383
384
|
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)
|
|
384
385
|
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/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)", page: browserHub() }); return json(res, await openSymbiotBrowser(b.url ? siteUrl(String(b.url)) : "")); }
|
|
386
387
|
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 }); }
|
|
388
|
+
// any site, added in Settings → Connections or the Symbiot Browser's address bar, or a card's / reply's "Sign in to <site>": linked and opened to sign in
|
|
389
|
+
if (u.pathname === "/api/links/site" && req.method === "POST") { const b = await readBody(req), here = b.here ? { open: async (url) => ({ ok: true, url }) } : undefined; return json(res, b.id ? await linkSite(String(b.id), here) : await addSite(String(b.site || ""), here)); }
|
|
387
390
|
if (u.pathname === "/api/links/check" && req.method === "POST") { const b = await readBody(req); return json(res, await checkLink(String(b.id || ""))); }
|
|
388
391
|
if (u.pathname === "/api/links/unlink" && req.method === "POST") { const b = await readBody(req); return json(res, unlinkSite(String(b.id || ""))); }
|
|
389
392
|
// Posts (post.mjs): the week's drafts, waiting on you. Approve is your yes: Marketing's
|
|
@@ -459,7 +462,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
459
462
|
try { const wav = await speak(b.text, b.lang || ""); res.writeHead(200, { "content-type": "audio/wav", "cache-control": "no-store", "content-length": wav.length }); return res.end(wav); }
|
|
460
463
|
catch (e) { return json(res, { error: String((e && e.message) || e) }); }
|
|
461
464
|
}
|
|
462
|
-
if (u.pathname === "/api/home/ask" && req.method === "POST") { const b = await readBody(req);
|
|
465
|
+
if (u.pathname === "/api/home/ask" && req.method === "POST") { const b = await readBody(req); const r = await homeAsk(b.question, { images: saveShots(b.images) }); let si = null; try { si = r && r.answer ? signInAsked(r.answer) : null; } catch {} return json(res, si ? { ...r, signInTo: si } : r); } // a reply asking you to sign in carries the button
|
|
463
466
|
if (u.pathname === "/api/adapt") return json(res, adaptState({ from: String(u.searchParams.get("from") || ""), commit: u.searchParams.get("commit") === "1", ...(u.searchParams.has("touch") ? { touch: u.searchParams.get("touch") === "1" } : {}) }));
|
|
464
467
|
if (u.pathname === "/api/adapt/use" && req.method === "POST") { const b = await readBody(req); return json(res, noteUse(b)); }
|
|
465
468
|
// Lanes (lanes.mjs): work agents handed to each other, and where it stands.
|
|
@@ -484,15 +487,21 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
484
487
|
// a Dashboard card's chat: go over what's new with your AI before a reply is drafted
|
|
485
488
|
if (u.pathname === "/api/watch/chat" && req.method === "POST") { const b = await readBody(req); return json(res, await boardChat(String(b.id || ""), b.question)); }
|
|
486
489
|
if (u.pathname === "/api/watch/chat/clear" && req.method === "POST") { const b = await readBody(req); return json(res, clearBoardChat(String(b.id || ""))); }
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
// On the phone: pair with the computer,
|
|
490
|
+
// Your phone (phone.mjs). On the computer: listen on your network for the phone
|
|
491
|
+
// (only on your click), a pairing code and its QR, the phones paired, the relay.
|
|
492
|
+
// On the phone: pair with the computer (typed, or a scanned link), ask it now
|
|
493
|
+
// (also when the phone's network changes), the copy of your work there, a change
|
|
494
|
+
// made on the phone (queued until the computer answers), forget it.
|
|
490
495
|
if (u.pathname === "/api/phone") return json(res, phoneState());
|
|
491
496
|
if (u.pathname === "/api/phone/link" && req.method === "POST") { const b = await readBody(req); return json(res, await setPhoneLink(b.on === true)); }
|
|
492
497
|
if (u.pathname === "/api/phone/code" && req.method === "POST") return json(res, newCode());
|
|
493
498
|
if (u.pathname === "/api/phone/unpair" && req.method === "POST") { const b = await readBody(req); return json(res, unpairPhone(String(b.id || ""))); }
|
|
494
|
-
if (u.pathname === "/api/phone/
|
|
499
|
+
if (u.pathname === "/api/phone/relay" && req.method === "POST") { const b = await readBody(req); return json(res, setRelay(b.on === true)); }
|
|
500
|
+
if (u.pathname === "/api/phone/pair" && req.method === "POST") { const b = await readBody(req); return json(res, await pairComputer(b.address, b.code, { link: String(b.link || "") })); }
|
|
495
501
|
if (u.pathname === "/api/phone/check" && req.method === "POST") return json(res, await pollComputer());
|
|
502
|
+
if (u.pathname === "/api/phone/find") return json(res, { found: await findComputers() });
|
|
503
|
+
if (u.pathname === "/api/phone/do" && req.method === "POST") { const b = await readBody(req); return json(res, queueChange(String(b.op || ""), b.args || {})); }
|
|
504
|
+
if (u.pathname === "/api/phone/note" && req.method === "POST") { const b = await readBody(req); return json(res, dismissNote(String(b.id || ""))); }
|
|
496
505
|
if (u.pathname === "/api/phone/forget" && req.method === "POST") return json(res, forgetComputer());
|
|
497
506
|
// What symbiot-desktop added (desktop.mjs): the weekly write-up, start at login.
|
|
498
507
|
if (u.pathname === "/api/desktop") return json(res, { weekly: weeklyState(), autostart: autostartState() });
|
|
@@ -567,6 +576,7 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
567
576
|
server.on("listening", () => {
|
|
568
577
|
if (announced) return; announced = true;
|
|
569
578
|
const url = `http://127.0.0.1:${server.address().port}/?t=${TOKEN}`;
|
|
579
|
+
setBrowserHub(`http://127.0.0.1:${server.address().port}/browser?t=${TOKEN}`); // the Symbiot Browser opens on its own page, served from here
|
|
570
580
|
const how = opened || RELAUNCH || process.env.SYMBIOT_NO_OPEN === "1" ? "" : openApp(url); opened = true; // only pop a window the first time (never in tests, never after an update)
|
|
571
581
|
console.log(`\n${c.g("●")} ${c.b("Symbiot")} is running at ${c.b(url)}`);
|
|
572
582
|
console.log(RELAUNCH ? c.d(" Restarted after an update; the open window reloads itself.") : how ? c.d(` Opened in ${/^[aeiou]/.test(how) ? "an" : "a"} ${how}.`) : c.d(" Open that URL in your browser."));
|
|
@@ -596,7 +606,10 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
|
|
|
596
606
|
checkLatest(); setInterval(checkLatest, 2 * 60 * 1000).unref(); // background update check (every 2 min)
|
|
597
607
|
startWeekly(writeup); // the weekly write-up + notification, when switched on in Settings
|
|
598
608
|
startWatches(); // pages you watch (Screens → Watch), read every few minutes
|
|
599
|
-
|
|
609
|
+
// Your phone: the computer listens if it's switched on, the phone asks if it's paired.
|
|
610
|
+
// An Approve from a paired phone is this same Approve, release notes and all.
|
|
611
|
+
setPhoneApprove(async (repo, untasked) => { const notes = await releaseNotes(releaseInput(repo, {})); return untasked ? approveChanges(repo, { tick: [], notes }) : approveRepo(repo, { notes }); });
|
|
612
|
+
startPhone();
|
|
600
613
|
setInterval(() => { try { startWaiting(); } catch {} }, 20000).unref(); // a run that waits for your step starts once the file it names changes
|
|
601
614
|
setInterval(() => { try { workTick(); } catch {} }, 20000).unref(); // a waiting task starts once its lane is free (config.autoStart: false leaves it to Go)
|
|
602
615
|
setInterval(() => { try { lanesTick(); } catch {} }, 20000).unref(); // agents hand work to other lanes, and hear back when it's done
|
package/tasks.mjs
CHANGED
|
@@ -50,7 +50,17 @@ function addTask(text, repo, { after = "" } = {}) {
|
|
|
50
50
|
const item = { id, text: line, ...(long ? { full } : {}), repo: repo || "", done: false, ts: Date.now() };
|
|
51
51
|
t.unshift(item); saveTasks(t); return item;
|
|
52
52
|
}
|
|
53
|
-
function toggleTask(id) { const t = loadTasks(); const it = t.find((x) => x.id === id); if (it) { it.done = !it.done; saveTasks(t); } return it || { error: "not found" }; }
|
|
53
|
+
function toggleTask(id) { const t = loadTasks(); const it = t.find((x) => x.id === id); if (it) { it.done = !it.done; it.changed = Date.now(); saveTasks(t); } return it || { error: "not found" }; }
|
|
54
|
+
// Done or not, as said (not flipped), for a change that can arrive late or twice
|
|
55
|
+
// (your phone's, phone.mjs): at is when it was made, and a tick here since wins.
|
|
56
|
+
function setTaskDone(id, done, { at = Date.now() } = {}) {
|
|
57
|
+
const t = loadTasks(), it = t.find((x) => x.id === id);
|
|
58
|
+
if (!it || it.archived) return { error: "not found" };
|
|
59
|
+
if (it.done === !!done) return it;
|
|
60
|
+
if ((Number(it.changed) || 0) > at) return { conflict: true, ...it };
|
|
61
|
+
it.done = !!done; it.changed = at; saveTasks(t);
|
|
62
|
+
return it;
|
|
63
|
+
}
|
|
54
64
|
function removeTask(id) { saveTasks(loadTasks().filter((x) => x.id !== id)); return { ok: true }; }
|
|
55
65
|
function restoreTask(id) { const t = loadTasks(); const it = t.find((x) => x.id === id); if (it) { it.archived = false; it.done = false; delete it.archivedAt; if (it.removedBy || it.dropped) { it.kept = true; delete it.removedBy; delete it.merged; delete it.dropped; } saveTasks(t); } return it || { error: "not found" }; }
|
|
56
66
|
// Which task texts the agent checked off in a repo's .symbiot/TASKS.md
|
|
@@ -700,4 +710,4 @@ function pushTasks(filter) {
|
|
|
700
710
|
return { empty: false, written, unresolved, handoff: handoffCmd() };
|
|
701
711
|
}
|
|
702
712
|
|
|
703
|
-
export { gitFailed, taskPaths, addTask, toggleTask, removeTask, restoreTask, completedInRepo, removalOf, applyRemovals, applyDrops, syncTasks, workingChanges, workingDiff, publishesOnMerge, unreleased, bumpOffer, learnNpm, releaseNeeded, withReleases, setVersion, changelogEntry, changelogSection, noteChangelog, releaseInput, shipWithBump, runSummary, saidFinished, pendingReview, commitSubject, shipChanges, autoMergeRepos, setAutoMerge, approveRepo, approveChanges, sendBack, TASK_ORDER, taskType, buildTasksMd, pushTasks };
|
|
713
|
+
export { gitFailed, taskPaths, addTask, toggleTask, setTaskDone, removeTask, restoreTask, completedInRepo, removalOf, applyRemovals, applyDrops, syncTasks, workingChanges, workingDiff, publishesOnMerge, unreleased, bumpOffer, learnNpm, releaseNeeded, withReleases, setVersion, changelogEntry, changelogSection, noteChangelog, releaseInput, shipWithBump, runSummary, saidFinished, pendingReview, commitSubject, shipChanges, autoMergeRepos, setAutoMerge, approveRepo, approveChanges, sendBack, TASK_ORDER, taskType, buildTasksMd, pushTasks };
|