persnally 2.5.2 → 2.6.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/build/src/cli.js +43 -11
- package/build/src/dashboard.html +283 -45
- package/build/src/lifecycle.d.ts +10 -0
- package/build/src/lifecycle.js +21 -0
- package/package.json +1 -1
package/build/src/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import { extractClaudeEvents, parseClaudeExport } from "./importers/claude.js";
|
|
|
19
19
|
import { DEFAULT_TRANSCRIPTS_DIR, extractClaudeCodeEvents, parseClaudeCodeTranscripts, } from "./importers/claude-code.js";
|
|
20
20
|
import { gitEvents, scanRepos } from "./importers/git.js";
|
|
21
21
|
import { freshConversations } from "./importers/extract.js";
|
|
22
|
-
import { autostartInstalled, installAutostart, LOG_FILE, removeAutostart, removePidFile, runningPid, startDetached, stopDaemon, writePidFile, } from "./lifecycle.js";
|
|
22
|
+
import { autostartInstalled, installAutostart, LOG_FILE, reloadAutostart, removeAutostart, removePidFile, runningPid, startDetached, stopDaemon, writePidFile, } from "./lifecycle.js";
|
|
23
23
|
import { newEvent } from "./events.js";
|
|
24
24
|
import { proseLines } from "./prose.js";
|
|
25
25
|
import { analyzeVoice } from "./stylometry.js";
|
|
@@ -50,6 +50,7 @@ Usage:
|
|
|
50
50
|
persnallyd activity Context-read engagement over time (retention pulse)
|
|
51
51
|
persnallyd start [--port N] Start the daemon in the background
|
|
52
52
|
persnallyd stop Stop the background daemon
|
|
53
|
+
persnallyd restart Restart the daemon (correctly handles autostart/launchd)
|
|
53
54
|
persnallyd serve [--port N] Run the daemon in the foreground (127.0.0.1:${DEFAULT_PORT})
|
|
54
55
|
persnallyd autostart [--remove] Start the daemon at login and keep it alive (macOS)
|
|
55
56
|
persnallyd config set-key <key> Store the Anthropic API key (owner-only file) for the daemon
|
|
@@ -171,13 +172,8 @@ async function main() {
|
|
|
171
172
|
console.error(`· Context hook skipped: ${e instanceof Error ? e.message : e}`);
|
|
172
173
|
}
|
|
173
174
|
}
|
|
174
|
-
console.log(`\nDone${imported ? ` — ${imported} events imported` : ""}
|
|
175
|
-
|
|
176
|
-
try {
|
|
177
|
-
execFileSync("open", [`http://127.0.0.1:${port}`]);
|
|
178
|
-
}
|
|
179
|
-
catch { /* non-fatal */ }
|
|
180
|
-
}
|
|
175
|
+
console.log(`\nDone${imported ? ` — ${imported} events imported` : ""}.`);
|
|
176
|
+
announceDashboard(port);
|
|
181
177
|
return;
|
|
182
178
|
}
|
|
183
179
|
case "scope": {
|
|
@@ -483,19 +479,43 @@ async function main() {
|
|
|
483
479
|
const existing = runningPid();
|
|
484
480
|
if (existing)
|
|
485
481
|
return die(`daemon already running (pid ${existing})`);
|
|
486
|
-
const
|
|
487
|
-
|
|
482
|
+
const port = parsePort(args);
|
|
483
|
+
const pid = await startDetached(process.argv[1], port);
|
|
484
|
+
console.log(`persnallyd started (pid ${pid}).`);
|
|
485
|
+
announceDashboard(port);
|
|
488
486
|
console.log(`Logs: ${LOG_FILE}`);
|
|
489
487
|
return;
|
|
490
488
|
}
|
|
491
489
|
case "stop": {
|
|
492
490
|
if (autostartInstalled()) {
|
|
493
|
-
console.error("Note: autostart is installed — launchd will
|
|
491
|
+
console.error("Note: autostart is installed — launchd will respawn the daemon. To restart cleanly use `persnallyd restart`; to stop it for good use `persnallyd autostart --remove`.");
|
|
494
492
|
}
|
|
495
493
|
const pid = await stopDaemon();
|
|
496
494
|
console.log(pid ? `Stopped daemon (pid ${pid}).` : "Daemon was not running.");
|
|
497
495
|
return;
|
|
498
496
|
}
|
|
497
|
+
case "restart": {
|
|
498
|
+
const port = parsePort(args);
|
|
499
|
+
if (autostartInstalled()) {
|
|
500
|
+
// launchd owns the lifecycle — a plain stop just gets respawned. Reload the
|
|
501
|
+
// job so it comes back on the current install (also heals a drifted plist path).
|
|
502
|
+
const health = await reloadAutostart(process.argv[1], port);
|
|
503
|
+
if (health) {
|
|
504
|
+
console.log(`Restarted via launchd — daemon up on v${health.version}.`);
|
|
505
|
+
announceDashboard(port);
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
console.log("Reloaded autostart; daemon is still coming up — check: persnallyd status");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
await stopDaemon();
|
|
513
|
+
const pid = await startDetached(process.argv[1], port);
|
|
514
|
+
console.log(`persnallyd restarted (pid ${pid}).`);
|
|
515
|
+
announceDashboard(port);
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
499
519
|
case "autostart": {
|
|
500
520
|
if (args[0] === "--remove") {
|
|
501
521
|
console.log(removeAutostart() ? "Autostart removed; daemon stopped." : "Autostart was not installed.");
|
|
@@ -507,6 +527,7 @@ async function main() {
|
|
|
507
527
|
console.log(`Stopped existing daemon (pid ${stopped}) — launchd takes over.`);
|
|
508
528
|
const plist = installAutostart(process.argv[1], parsePort(args));
|
|
509
529
|
console.log(`Autostart installed (${plist}). The daemon now runs at login and restarts if it exits.`);
|
|
530
|
+
announceDashboard(parsePort(args), false); // launchd brings it up async — show the link, don't open a not-yet-ready page
|
|
510
531
|
return;
|
|
511
532
|
}
|
|
512
533
|
case "serve": {
|
|
@@ -553,6 +574,17 @@ function summarize(payload) {
|
|
|
553
574
|
const s = JSON.stringify(payload);
|
|
554
575
|
return s.length > 80 ? s.slice(0, 77) + "..." : s;
|
|
555
576
|
}
|
|
577
|
+
/** Print the dashboard URL and, when run interactively on macOS, open it. */
|
|
578
|
+
function announceDashboard(port, open = true) {
|
|
579
|
+
const url = `http://127.0.0.1:${port}`;
|
|
580
|
+
console.log(`Dashboard: ${url}`);
|
|
581
|
+
if (open && process.platform === "darwin" && process.stdout.isTTY) {
|
|
582
|
+
try {
|
|
583
|
+
execFileSync("open", [url]);
|
|
584
|
+
}
|
|
585
|
+
catch { /* non-fatal — the link is printed above */ }
|
|
586
|
+
}
|
|
587
|
+
}
|
|
556
588
|
function die(msg) {
|
|
557
589
|
console.error(msg);
|
|
558
590
|
process.exit(1);
|
package/build/src/dashboard.html
CHANGED
|
@@ -193,6 +193,32 @@
|
|
|
193
193
|
.delta .sep { display: none; } /* stacked on phones — separators only read inline */
|
|
194
194
|
}
|
|
195
195
|
@media (prefers-reduced-motion: reduce) { .reveal { animation: none; opacity: 1; transform: none; } .read.fresh { animation: none; } }
|
|
196
|
+
|
|
197
|
+
/* ── hero intro (plain-language "what am I looking at") ── */
|
|
198
|
+
.hero-sub { font-size: 14.5px; color: var(--dim); margin-top: 14px; max-width: 64ch; line-height: 1.65; }
|
|
199
|
+
.hero-sub b { color: var(--text); font-weight: 600; }
|
|
200
|
+
|
|
201
|
+
/* ── shareable portrait card ── */
|
|
202
|
+
.modal { position: fixed; inset: 0; z-index: 50; display: none; align-items: center; justify-content: center;
|
|
203
|
+
padding: 24px; background: rgba(5,6,8,0.80); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
|
|
204
|
+
.modal.open { display: flex; }
|
|
205
|
+
.modal-inner { background: var(--panel); border: 1px solid var(--line-2); border-radius: 16px; width: 100%;
|
|
206
|
+
max-width: 860px; max-height: 92vh; overflow: auto; padding: 22px 24px; display: grid; gap: 20px; grid-template-columns: 1fr; }
|
|
207
|
+
@media (min-width: 760px) { .modal-inner { grid-template-columns: minmax(0,330px) 1fr; align-items: start; } }
|
|
208
|
+
.modal-head { grid-column: 1 / -1; display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
|
|
209
|
+
.modal-head h3 { font-size: 16px; font-weight: 600; }
|
|
210
|
+
.modal-head .x { background: none; border: none; color: var(--dim); font-size: 24px; cursor: pointer; line-height: 1; }
|
|
211
|
+
.modal-head .x:hover { color: var(--text); }
|
|
212
|
+
.card-preview { border-radius: 12px; overflow: hidden; border: 1px solid var(--line); background: #000; align-self: start; }
|
|
213
|
+
.card-preview canvas { display: block; width: 100%; height: auto; }
|
|
214
|
+
.card-ctrls { display: flex; flex-direction: column; gap: 18px; }
|
|
215
|
+
.card-ctrls .sub { font-size: 13px; color: var(--dim); line-height: 1.6; }
|
|
216
|
+
.toggles { display: flex; flex-direction: column; gap: 11px; }
|
|
217
|
+
.tog { display: flex; align-items: center; gap: 10px; font-size: 14px; color: var(--text); cursor: pointer; user-select: none; }
|
|
218
|
+
.tog input { width: 16px; height: 16px; accent-color: #FFFFFF; cursor: pointer; }
|
|
219
|
+
.card-actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
220
|
+
.privacy-note { font-size: 11.5px; color: var(--faint); line-height: 1.65; }
|
|
221
|
+
.privacy-note b { color: var(--dim); }
|
|
196
222
|
</style>
|
|
197
223
|
</head>
|
|
198
224
|
<body>
|
|
@@ -206,7 +232,8 @@
|
|
|
206
232
|
<a class="btn ghost" href="https://github.com/sidpan2011/persnally" target="_blank" rel="noopener">GitHub <span class="arr">↗</span></a>
|
|
207
233
|
<a class="btn ghost" href="https://persnally.com" target="_blank" rel="noopener">persnally.com <span class="arr">↗</span></a>
|
|
208
234
|
<button class="btn ghost" id="reflect">Reflect</button>
|
|
209
|
-
<button class="btn" id="synthesize">Re-synthesize</button>
|
|
235
|
+
<button class="btn ghost" id="synthesize">Re-synthesize</button>
|
|
236
|
+
<button class="btn" id="shareBtn">Share portrait <span class="arr" style="opacity:.75">✦</span></button>
|
|
210
237
|
</div>
|
|
211
238
|
</header>
|
|
212
239
|
|
|
@@ -215,6 +242,7 @@
|
|
|
215
242
|
<div class="hero reveal" style="animation-delay:.05s">
|
|
216
243
|
<div class="scale-beat" id="scaleBeat"></div>
|
|
217
244
|
<div class="archetype" id="archetype">Reading your context…</div>
|
|
245
|
+
<div class="hero-sub">Built from <b>your own AI history</b> — your tools read this so they stop treating you like a stranger, and <b>every byte stays on your machine</b>.</div>
|
|
218
246
|
<div class="gen-meta" id="genMeta"></div>
|
|
219
247
|
</div>
|
|
220
248
|
|
|
@@ -234,7 +262,7 @@
|
|
|
234
262
|
<div class="view-toggle"><button id="vGraph" class="on">map</button><button id="vList">list</button></div>
|
|
235
263
|
</div>
|
|
236
264
|
<div class="constellation-wrap">
|
|
237
|
-
<div class="constellation" id="graphHost"><canvas id="graph"></canvas><div class="legend" id="legend"></div><div class="ghint">
|
|
265
|
+
<div class="constellation" id="graphHost"><canvas id="graph"></canvas><div class="legend" id="legend"></div><div class="ghint">interests by strength · drag · zoom</div></div>
|
|
238
266
|
<div class="card" id="topicList" style="display:none;padding:6px 14px"><table id="topics"></table></div>
|
|
239
267
|
<div class="node-pop" id="nodePop"></div>
|
|
240
268
|
</div>
|
|
@@ -262,6 +290,30 @@
|
|
|
262
290
|
</div>
|
|
263
291
|
<div class="preview-ribbon" id="ribbon">preview data — start <code>persnallyd</code> and reload to see your own</div>
|
|
264
292
|
|
|
293
|
+
<div class="modal" id="shareModal" aria-hidden="true">
|
|
294
|
+
<div class="modal-inner" role="dialog" aria-label="Share your portrait">
|
|
295
|
+
<div class="modal-head">
|
|
296
|
+
<h3>Share your portrait</h3>
|
|
297
|
+
<button class="x" id="shareClose" aria-label="Close">×</button>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="card-preview"><canvas id="cardCanvas"></canvas></div>
|
|
300
|
+
<div class="card-ctrls">
|
|
301
|
+
<div class="sub">A snapshot of who your AI knows you to be. Pick what's on it — drawn here on your machine, never uploaded.</div>
|
|
302
|
+
<div class="toggles">
|
|
303
|
+
<label class="tog"><input type="checkbox" id="tHead" checked> Headline — your archetype</label>
|
|
304
|
+
<label class="tog"><input type="checkbox" id="tInt" checked> Top interests</label>
|
|
305
|
+
<label class="tog"><input type="checkbox" id="tVoice" checked> How you write</label>
|
|
306
|
+
<label class="tog"><input type="checkbox" id="tStats" checked> Stats</label>
|
|
307
|
+
</div>
|
|
308
|
+
<div class="card-actions">
|
|
309
|
+
<button class="btn" id="cardDownload">Download PNG</button>
|
|
310
|
+
<button class="btn ghost" id="cardCopy">Copy image</button>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="privacy-note"><b>Nothing is uploaded</b> — the image is rendered locally and saved by you. The footer credits persnally.com so others can find it.</div>
|
|
313
|
+
</div>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
265
317
|
<script>
|
|
266
318
|
"use strict";
|
|
267
319
|
const $ = (id) => document.getElementById(id);
|
|
@@ -488,16 +540,37 @@ function renderMap(topics) {
|
|
|
488
540
|
const resize = () => { w = host.clientWidth; h = window.innerWidth < 640 ? 380 : 520; canvas.width = w*dpr; canvas.height = h*dpr; canvas.style.height = h+"px"; };
|
|
489
541
|
resize();
|
|
490
542
|
|
|
491
|
-
const
|
|
492
|
-
const
|
|
493
|
-
const cx = w/2, cy = h/2;
|
|
494
|
-
|
|
495
|
-
const
|
|
496
|
-
|
|
497
|
-
const
|
|
498
|
-
|
|
543
|
+
const INTERESTS = data.slice(0, 12);
|
|
544
|
+
const maxW = Math.max(...INTERESTS.map(t=>t.weight), 0.01);
|
|
545
|
+
const cx = w/2, cy = h/2, ring = Math.min(w,h)*0.27;
|
|
546
|
+
const lerp = (a,b,t)=>a+(b-a)*t;
|
|
547
|
+
const warmRGB = (t)=>[Math.round(lerp(255,200,t)),Math.round(lerp(198,140,t)),Math.round(lerp(120,70,t))]; // curated amber: bright gold (strongest) → bronze (lighter), one warm family
|
|
548
|
+
const rgba = (c,a)=>`rgba(${c[0]},${c[1]},${c[2]},${a})`;
|
|
549
|
+
const lighten = (c,a)=>[Math.min(255,c[0]+a),Math.min(255,c[1]+a),Math.min(255,c[2]+a)];
|
|
550
|
+
// radial portrait: YOU at the center, interests radiating by strength, their entities as leaf nodes
|
|
551
|
+
const center = { kind:"you", x:cx, y:cy, vx:0, vy:0, r:23, rgb:[255,209,130], fixed:true };
|
|
552
|
+
const interests = INTERESTS.map((t,i) => {
|
|
553
|
+
const ang = (i/INTERESTS.length)*Math.PI*2 - Math.PI/2;
|
|
554
|
+
return { kind:"topic", t, ang, x:cx+Math.cos(ang)*ring, y:cy+Math.sin(ang)*ring, vx:0, vy:0,
|
|
555
|
+
r:9+Math.sqrt(t.weight/maxW)*15, rgb:warmRGB(INTERESTS.length>1 ? i/(INTERESTS.length-1) : 0) };
|
|
556
|
+
});
|
|
557
|
+
const leaves = [];
|
|
558
|
+
interests.forEach((nd, ii) => {
|
|
559
|
+
(nd.t.entities||[]).slice(0,2).forEach((e,k,arr) => {
|
|
560
|
+
const a = nd.ang + (k-(arr.length-1)/2)*0.55;
|
|
561
|
+
leaves.push({ kind:"entity", label:e, x:cx+Math.cos(a)*ring*1.65, y:cy+Math.sin(a)*ring*1.65, vx:0, vy:0, r:4, rgb:[176,196,224], parent:1+ii });
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
const nodes = [center, ...interests, ...leaves];
|
|
565
|
+
const I0 = 1, L0 = 1 + interests.length;
|
|
499
566
|
const edges = [];
|
|
500
|
-
|
|
567
|
+
interests.forEach((_,ii) => edges.push([0, I0+ii, "trunk"]));
|
|
568
|
+
// synapses: interests that share an entity wire together → a neural web, not just a tree
|
|
569
|
+
for (let i=0;i<interests.length;i++) for (let j=i+1;j<interests.length;j++) {
|
|
570
|
+
const ei=interests[i].t.entities||[], ej=interests[j].t.entities||[];
|
|
571
|
+
if (ei.length && ej.length && ei.some(e=>ej.includes(e))) edges.push([I0+i, I0+j, "synapse"]);
|
|
572
|
+
}
|
|
573
|
+
leaves.forEach((lf,li) => edges.push([lf.parent, L0+li, "branch"]));
|
|
501
574
|
const neighbors = nodes.map(()=>new Set());
|
|
502
575
|
edges.forEach(([a,b])=>{ neighbors[a].add(b); neighbors[b].add(a); });
|
|
503
576
|
|
|
@@ -507,63 +580,87 @@ function renderMap(topics) {
|
|
|
507
580
|
|
|
508
581
|
function tick() {
|
|
509
582
|
let energy = 0;
|
|
510
|
-
for (let i=
|
|
511
|
-
const A=nodes[i]; if (i===dragNode) continue;
|
|
583
|
+
for (let i=1;i<nodes.length;i++) {
|
|
584
|
+
const A=nodes[i]; if (i===dragNode || A.fixed) continue;
|
|
512
585
|
for (let j=0;j<nodes.length;j++) { if (i===j) continue; const B=nodes[j];
|
|
513
586
|
let dx=A.x-B.x, dy=A.y-B.y, d2=dx*dx+dy*dy||1, d=Math.sqrt(d2);
|
|
514
|
-
const rep=Math.min(
|
|
515
|
-
const minD=A.r+B.r+
|
|
587
|
+
const rep=Math.min(5000/d2, 6); dx/=d; dy/=d; A.vx+=dx*rep; A.vy+=dy*rep;
|
|
588
|
+
const minD=A.r+B.r+14; if (d<minD) { const p=(minD-d)/2; A.x+=dx*p; A.y+=dy*p; }
|
|
516
589
|
}
|
|
517
|
-
const an=anchor[A.t.category]; A.vx+=(an.x-A.x)*0.014; A.vy+=(an.y-A.y)*0.014; // cluster pull
|
|
518
590
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
nodes.
|
|
591
|
+
interests.forEach((nd) => { if (nd===nodes[dragNode]) return; const tx=cx+Math.cos(nd.ang)*ring, ty=cy+Math.sin(nd.ang)*ring; nd.vx+=(tx-nd.x)*0.022; nd.vy+=(ty-nd.y)*0.022; });
|
|
592
|
+
leaves.forEach((lf) => { if (lf===nodes[dragNode]) return; const p=nodes[lf.parent]; const a=Math.atan2(p.y-cy,p.x-cx)||0; const tx=p.x+Math.cos(a)*44, ty=p.y+Math.sin(a)*44; lf.vx+=(tx-lf.x)*0.05; lf.vy+=(ty-lf.y)*0.05; });
|
|
593
|
+
for (let i=1;i<nodes.length;i++) { const n=nodes[i]; if (i===dragNode || n.fixed) continue; n.vx*=0.84; n.vy*=0.84; n.x+=n.vx; n.y+=n.vy; energy+=n.vx*n.vx+n.vy*n.vy; }
|
|
522
594
|
return energy;
|
|
523
595
|
}
|
|
524
|
-
function draw() {
|
|
596
|
+
function draw(now) {
|
|
597
|
+
now = now || performance.now(); const T = now * 0.001;
|
|
525
598
|
ctx.setTransform(dpr,0,0,dpr,0,0); ctx.clearRect(0,0,w,h);
|
|
526
|
-
|
|
599
|
+
// backdrop: faint warm core light, cool fall-off — depth, not flat black
|
|
600
|
+
const bg=ctx.createRadialGradient(w/2,h*0.46,0,w/2,h*0.46,Math.max(w,h)*0.72);
|
|
601
|
+
bg.addColorStop(0,"rgba(255,176,96,0.05)"); bg.addColorStop(0.55,"rgba(16,18,22,0)"); bg.addColorStop(1,"rgba(6,7,9,0.55)");
|
|
602
|
+
ctx.fillStyle=bg; ctx.fillRect(0,0,w,h);
|
|
603
|
+
// gentle perpetual drift — alive without bouncing; used everywhere so nothing desyncs
|
|
604
|
+
const amp = reduced?0:2.4;
|
|
605
|
+
const P = nodes.map((n,i)=>({ x:n.x+((n.fixed||i===dragNode)?0:Math.sin(T*0.5+i*1.1)*amp), y:n.y+((n.fixed||i===dragNode)?0:Math.cos(T*0.42+i*1.7)*amp) }));
|
|
527
606
|
ctx.save(); ctx.translate(cam.x,cam.y); ctx.scale(cam.k,cam.k);
|
|
528
|
-
//
|
|
529
|
-
edges.forEach(([a,b]) => { const A=
|
|
530
|
-
let dx=B.x-A.x,dy=B.y-A.y,d=Math.hypot(dx,dy)||1; const off=d*0.
|
|
531
|
-
const al
|
|
532
|
-
const g=ctx.createLinearGradient(A.x,A.y,B.x,B.y); g.addColorStop(0,
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
ctx.
|
|
539
|
-
ctx.fillStyle=
|
|
607
|
+
// connections — thin, elegant; warm trunks, synapses, faint dendrites
|
|
608
|
+
edges.forEach(([a,b,kind]) => { const A=P[a],B=P[b]; const active=hover===-1||hover===a||hover===b||neighbors[hover].has(a)||neighbors[hover].has(b);
|
|
609
|
+
let dx=B.x-A.x,dy=B.y-A.y,d=Math.hypot(dx,dy)||1; const off=d*0.09, mx=(A.x+B.x)/2+(-dy/d)*off, my=(A.y+B.y)/2+(dx/d)*off;
|
|
610
|
+
if (kind==="trunk") { const al=hover===-1?0.4:(active?0.8:0.06); const g=ctx.createLinearGradient(A.x,A.y,B.x,B.y); g.addColorStop(0,rgba([255,150,80],al)); g.addColorStop(1,rgba(nodes[b].rgb,al*0.7)); ctx.strokeStyle=g; ctx.lineWidth=(active&&hover!==-1?2:1.1)/cam.k; }
|
|
611
|
+
else if (kind==="synapse") { const al=hover===-1?0.12:(active?0.42:0.03); const g=ctx.createLinearGradient(A.x,A.y,B.x,B.y); g.addColorStop(0,rgba(nodes[a].rgb,al)); g.addColorStop(1,rgba(nodes[b].rgb,al)); ctx.strokeStyle=g; ctx.lineWidth=0.9/cam.k; }
|
|
612
|
+
else { const al=hover===-1?0.16:(active?0.45:0.03); ctx.strokeStyle=rgba(nodes[a].rgb,al); ctx.lineWidth=0.8/cam.k; }
|
|
613
|
+
ctx.beginPath(); ctx.moveTo(A.x,A.y); ctx.quadraticCurveTo(mx,my,B.x,B.y); ctx.stroke(); });
|
|
614
|
+
// restrained, steady glow
|
|
615
|
+
ctx.globalCompositeOperation="lighter";
|
|
616
|
+
nodes.forEach((n,i) => { const active=hover===-1||hover===i||neighbors[hover].has(i); const k=active?1:0.1; const R=n.r*2.2;
|
|
617
|
+
const halo=ctx.createRadialGradient(P[i].x,P[i].y,0,P[i].x,P[i].y,R); halo.addColorStop(0,rgba(n.rgb,0.4*k)); halo.addColorStop(0.5,rgba(n.rgb,0.1*k)); halo.addColorStop(1,rgba(n.rgb,0));
|
|
618
|
+
ctx.fillStyle=halo; ctx.beginPath(); ctx.arc(P[i].x,P[i].y,R,0,Math.PI*2); ctx.fill(); });
|
|
619
|
+
ctx.globalCompositeOperation="source-over";
|
|
620
|
+
// refined cores — soft inner light + thin rim, no glossy bead
|
|
621
|
+
nodes.forEach((n,i) => { const active=hover===-1||hover===i||neighbors[hover].has(i); ctx.globalAlpha=active?1:0.22;
|
|
622
|
+
const x=P[i].x, y=P[i].y, r=n.r;
|
|
623
|
+
const cg=ctx.createRadialGradient(x-r*0.3,y-r*0.35,r*0.1,x,y,r); cg.addColorStop(0,rgba(lighten(n.rgb,45),1)); cg.addColorStop(1,rgba(n.rgb,1));
|
|
624
|
+
ctx.fillStyle=cg; ctx.beginPath(); ctx.arc(x,y,r,0,Math.PI*2); ctx.fill();
|
|
625
|
+
ctx.lineWidth=1/cam.k; ctx.strokeStyle=rgba(lighten(n.rgb,70),i===hover?0.9:0.4); ctx.stroke(); });
|
|
540
626
|
ctx.globalAlpha=1; ctx.restore();
|
|
541
|
-
// labels
|
|
542
|
-
ctx.
|
|
627
|
+
// labels — clean, tracking the drifted node; entities only on hover
|
|
628
|
+
ctx.textAlign="center"; const placed=[];
|
|
543
629
|
nodes.map((_,i)=>i).sort((a,b)=>nodes[b].r-nodes[a].r).forEach(i => { const n=nodes[i];
|
|
544
630
|
const active = hover===-1 || hover===i || neighbors[hover].has(i);
|
|
545
|
-
|
|
631
|
+
if (n.kind==="entity" && hover===-1) return;
|
|
546
632
|
if (!active && hover!==-1) return;
|
|
547
|
-
const
|
|
633
|
+
const label = n.kind==="you" ? "you" : (n.kind==="topic" ? n.t.topic : n.label);
|
|
634
|
+
ctx.font = (n.kind==="you" ? "600 12.5px " : "500 11.5px ") + FONT;
|
|
635
|
+
const sx=W2Sx(P[i].x), sy=W2Sy(P[i].y), sr=n.r*cam.k, tw=ctx.measureText(label).width;
|
|
636
|
+
const force = i===hover || n.kind==="you" || (hover!==-1 && neighbors[hover].has(i));
|
|
548
637
|
const rect={x:sx-tw/2-3,y:sy+sr+3,w:tw+6,h:15};
|
|
549
638
|
const hit=placed.some(p=>!(rect.x+rect.w<p.x||rect.x>p.x+p.w||rect.y+rect.h<p.y||rect.y>p.y+p.h));
|
|
550
639
|
if (hit && !force) return; placed.push(rect);
|
|
551
|
-
ctx.globalAlpha = hover===-1?0.
|
|
640
|
+
ctx.globalAlpha = hover===-1?(n.kind==="you"?1:0.82):(active?1:0.2); ctx.fillStyle = (i===hover||n.kind==="you")?"#FFFFFF":"#C7CBD2"; ctx.fillText(label, sx, sy+sr+15);
|
|
552
641
|
});
|
|
553
642
|
ctx.globalAlpha=1;
|
|
554
643
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
644
|
+
let physicsSettled=false;
|
|
645
|
+
function loop(now) { if (gen!==mapGen) return;
|
|
646
|
+
if (!physicsSettled) { const e=tick(); frames++; if ((e<0.02 && frames>40) || frames>360) physicsSettled=true; }
|
|
647
|
+
draw(now);
|
|
648
|
+
if (!reduced || !physicsSettled) requestAnimationFrame(loop); else alive=false; // keep firing once settled (paused automatically when the tab is hidden)
|
|
649
|
+
}
|
|
650
|
+
if (reduced) { for (let s=0;s<200;s++) tick(); physicsSettled=true; draw(performance.now()); alive=false; } else { alive=true; requestAnimationFrame(loop); }
|
|
651
|
+
function wake() { if (!alive && gen===mapGen) { alive=true; physicsSettled=false; frames=320; requestAnimationFrame(loop); } }
|
|
558
652
|
|
|
559
|
-
$("legend").innerHTML =
|
|
653
|
+
$("legend").innerHTML = `<span><i style="color:#FF9637"></i><span style="color:var(--dim)">you</span></span><span><i style="color:#FFB454"></i><span style="color:var(--dim)">strongest</span></span><span><i style="color:#6EAAFF"></i><span style="color:var(--dim)">lighter</span></span><span><i style="color:#BCD4FF"></i><span style="color:var(--dim)">the specifics</span></span>`;
|
|
560
654
|
|
|
561
655
|
const pop=$("nodePop");
|
|
562
656
|
const nodeAt=(mx,my)=>{ const wx=S2Wx(mx),wy=S2Wy(my); for (let i=nodes.length-1;i>=0;i--) if (Math.hypot(wx-nodes[i].x,wy-nodes[i].y)<=nodes[i].r+8/cam.k) return i; return -1; };
|
|
563
657
|
const rel=(e)=>{ const r=canvas.getBoundingClientRect(); return [e.clientX-r.left, e.clientY-r.top]; };
|
|
564
|
-
function showPop(n) { const
|
|
565
|
-
|
|
566
|
-
|
|
658
|
+
function showPop(n) { const nd=nodes[n];
|
|
659
|
+
if (nd.kind==="topic") { const p=nd.t;
|
|
660
|
+
pop.innerHTML=`<div class="npt">${esc(p.topic)}</div><div class="npm">${esc(p.category)} · ${esc(p.dominant_intent)} · weight ${p.weight.toFixed(2)} · ${p.signals}×</div>${(p.entities||[]).length?`<div class="npe">${esc(p.entities.slice(0,4).join(", "))}</div>`:""}`;
|
|
661
|
+
} else if (nd.kind==="entity") { pop.innerHTML=`<div class="npt">${esc(nd.label)}</div><div class="npm">a specific within ${esc((nodes[nd.parent].t||{}).topic||"this interest")}</div>`;
|
|
662
|
+
} else { pop.innerHTML=`<div class="npt">you</div><div class="npm">${interests.length} interests · ${leaves.length} specifics, sized by strength</div>`; }
|
|
663
|
+
pop.style.left=Math.min(W2Sx(nd.x)+14,w-250)+"px"; pop.style.top=(W2Sy(nd.y)+14)+"px"; pop.classList.add("show"); }
|
|
567
664
|
|
|
568
665
|
// Pointer events unify mouse + touch + pen; a second pointer drives pinch-zoom, a tap inspects.
|
|
569
666
|
const pts=new Map();
|
|
@@ -645,6 +742,7 @@ async function loadAll() {
|
|
|
645
742
|
renderReads(reads, total);
|
|
646
743
|
renderEngage(activity);
|
|
647
744
|
renderReflections(assertions);
|
|
745
|
+
setCardData(profile, topics, voice, stats);
|
|
648
746
|
saveSnapshot(topics);
|
|
649
747
|
}
|
|
650
748
|
|
|
@@ -667,6 +765,146 @@ setInterval(async () => {
|
|
|
667
765
|
if (activity) renderEngage(activity);
|
|
668
766
|
}, 25000);
|
|
669
767
|
|
|
768
|
+
/* ── shareable portrait card (drawn locally on a canvas; nothing uploaded) ── */
|
|
769
|
+
let CARD = null;
|
|
770
|
+
const CARD_W = 1080, CARD_H = 1350;
|
|
771
|
+
function rrect(ctx, x, y, w, h, r) {
|
|
772
|
+
ctx.beginPath(); ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);
|
|
773
|
+
ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();
|
|
774
|
+
}
|
|
775
|
+
function wrapLines(ctx, text, maxW) {
|
|
776
|
+
const out = []; let line = "";
|
|
777
|
+
for (const word of String(text).split(/\s+/)) {
|
|
778
|
+
const test = line ? line + " " + word : word;
|
|
779
|
+
if (ctx.measureText(test).width > maxW && line) { out.push(line); line = word; } else line = test;
|
|
780
|
+
}
|
|
781
|
+
if (line) out.push(line); return out;
|
|
782
|
+
}
|
|
783
|
+
function condenseVoice(voice) {
|
|
784
|
+
if (!voice || !voice.items) return [];
|
|
785
|
+
const picks = voice.items.filter((it) => ["voice","format","emphasis"].includes(it.dimension))
|
|
786
|
+
.sort((a,b) => (b.confidence||0) - (a.confidence||0))
|
|
787
|
+
.map((it) => it.pattern.split("—")[0].split(";")[0].split("(")[0].trim());
|
|
788
|
+
return [...new Set(picks)].slice(0, 4);
|
|
789
|
+
}
|
|
790
|
+
function setCardData(profile, topics, voice, stats) {
|
|
791
|
+
const s = stats || {};
|
|
792
|
+
CARD = {
|
|
793
|
+
headline: (profile && profile.headline) || "A portrait in progress.",
|
|
794
|
+
interests: (topics||[]).slice(0,6).map((t) => ({ label: t.topic, color: catColor(t.category) })),
|
|
795
|
+
voice: condenseVoice(voice),
|
|
796
|
+
signals: s.total || 0,
|
|
797
|
+
days: s.first ? Math.max(1, Math.round((new Date(s.last||Date.now()) - new Date(s.first)) / 86400000)) : 0,
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function buildCard(canvas, opts) {
|
|
801
|
+
const c = CARD || { headline:"", interests:[], voice:[], signals:0, days:0 };
|
|
802
|
+
const dpr = 2, W = CARD_W, H = CARD_H, PAD = 84;
|
|
803
|
+
canvas.width = W*dpr; canvas.height = H*dpr; canvas.style.aspectRatio = (W/H).toFixed(4);
|
|
804
|
+
const ctx = canvas.getContext("2d"); ctx.setTransform(dpr,0,0,dpr,0,0);
|
|
805
|
+
ctx.textBaseline = "alphabetic"; ctx.textAlign = "left";
|
|
806
|
+
ctx.fillStyle = "#0B0C0E"; ctx.fillRect(0,0,W,H);
|
|
807
|
+
// ambient glows tinted by the user's own interest colors — on-brand, "looks good on camera"
|
|
808
|
+
const cols = c.interests.map((i) => i.color);
|
|
809
|
+
[[W*0.20,H*0.15,cols[0]||"#5EC8FF"],[W*0.86,H*0.30,cols[1]||"#FFB454"],[W*0.5,H*0.95,cols[2]||"#B388FF"]]
|
|
810
|
+
.forEach(([x,y,col]) => { const g=ctx.createRadialGradient(x,y,0,x,y,W*0.55); g.addColorStop(0,col+"26"); g.addColorStop(1,col+"00"); ctx.fillStyle=g; ctx.fillRect(0,0,W,H); });
|
|
811
|
+
ctx.strokeStyle = "rgba(255,255,255,0.10)"; ctx.lineWidth = 2; rrect(ctx,28,28,W-56,H-56,20); ctx.stroke();
|
|
812
|
+
|
|
813
|
+
let y = PAD + 6;
|
|
814
|
+
ctx.fillStyle = "#F1F2F4"; ctx.font = "600 30px "+FONT; ctx.fillText("persnally", PAD, y);
|
|
815
|
+
ctx.fillStyle = "#35D07F"; ctx.beginPath(); ctx.arc(PAD + ctx.measureText("persnally").width + 15, y-9, 5, 0, Math.PI*2); ctx.fill();
|
|
816
|
+
ctx.textAlign = "right"; ctx.fillStyle = "#9A9DA5"; ctx.font = "500 22px "+FONT; ctx.fillText("a portrait of me", W-PAD, y); ctx.textAlign = "left";
|
|
817
|
+
y += 50;
|
|
818
|
+
|
|
819
|
+
if (opts.head && c.headline) {
|
|
820
|
+
let fs = 54, lines;
|
|
821
|
+
do { ctx.font = "700 "+fs+"px "+FONT; lines = wrapLines(ctx, c.headline, W-PAD*2); fs -= 3; } while (lines.length > 4 && fs > 30);
|
|
822
|
+
fs += 3; ctx.font = "700 "+fs+"px "+FONT; ctx.fillStyle = "#FFFFFF";
|
|
823
|
+
const lh = fs*1.18;
|
|
824
|
+
for (const ln of lines.slice(0,4)) { y += lh; ctx.fillText(ln, PAD, y); }
|
|
825
|
+
y += 24;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (opts.int && c.interests.length) {
|
|
829
|
+
y += 30; ctx.fillStyle = "#9A9DA5"; ctx.font = "600 19px "+FONT; ctx.fillText("WHAT I'M INTO", PAD, y); y += 36;
|
|
830
|
+
let x = PAD; const ch = 52, gap = 13; ctx.font = "600 24px "+FONT;
|
|
831
|
+
for (const it of c.interests) {
|
|
832
|
+
const cw = ctx.measureText(it.label).width + 50;
|
|
833
|
+
if (x + cw > W-PAD) { x = PAD; y += ch + gap; }
|
|
834
|
+
ctx.fillStyle = it.color+"22"; ctx.strokeStyle = it.color+"99"; ctx.lineWidth = 1.5; rrect(ctx,x,y,cw,ch,ch/2); ctx.fill(); ctx.stroke();
|
|
835
|
+
ctx.fillStyle = it.color; ctx.beginPath(); ctx.arc(x+22, y+ch/2, 5, 0, Math.PI*2); ctx.fill();
|
|
836
|
+
ctx.fillStyle = "#F1F2F4"; ctx.fillText(it.label, x+38, y+ch/2+8);
|
|
837
|
+
x += cw + gap;
|
|
838
|
+
}
|
|
839
|
+
y += ch + 26;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (opts.voice && c.voice.length) {
|
|
843
|
+
y += 24; ctx.fillStyle = "#9A9DA5"; ctx.font = "600 19px "+FONT; ctx.fillText("HOW I WRITE", PAD, y); y += 38;
|
|
844
|
+
ctx.fillStyle = "#C7CACF"; ctx.font = "400 26px "+FONT;
|
|
845
|
+
for (const ln of wrapLines(ctx, c.voice.join(" · "), W-PAD*2).slice(0,2)) { ctx.fillText(ln, PAD, y); y += 38; }
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// constellation — the brand's signature visual, drawn from the same interest colors;
|
|
849
|
+
// fills the lower band so the card reads full in every toggle state.
|
|
850
|
+
if (c.interests.length >= 2) {
|
|
851
|
+
const bandTop = y + 30, bandBottom = H - PAD - 104;
|
|
852
|
+
if (bandBottom - bandTop > 130) {
|
|
853
|
+
const cxb = W/2, cyb = (bandTop + bandBottom) / 2;
|
|
854
|
+
const spread = Math.min(W - PAD*2, bandBottom - bandTop) * 0.46;
|
|
855
|
+
const ns = c.interests.map((it, i) => {
|
|
856
|
+
const a = i * 2.39996323, rad = spread * Math.sqrt((i + 0.5) / c.interests.length);
|
|
857
|
+
return { x: cxb + Math.cos(a)*rad, y: cyb + Math.sin(a)*rad*0.62, r: 30 - i*3.2, color: it.color };
|
|
858
|
+
});
|
|
859
|
+
for (let i = 0; i < ns.length; i++) for (let j = i+1; j < ns.length; j++) {
|
|
860
|
+
const A = ns[i], B = ns[j], d = Math.hypot(A.x-B.x, A.y-B.y);
|
|
861
|
+
if (d > spread*1.05) continue;
|
|
862
|
+
const g = ctx.createLinearGradient(A.x,A.y,B.x,B.y); g.addColorStop(0,A.color+"40"); g.addColorStop(1,B.color+"40");
|
|
863
|
+
const off = d*0.14, mx = (A.x+B.x)/2 + (-(B.y-A.y)/d)*off, my = (A.y+B.y)/2 + ((B.x-A.x)/d)*off;
|
|
864
|
+
ctx.strokeStyle = g; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.moveTo(A.x,A.y); ctx.quadraticCurveTo(mx,my,B.x,B.y); ctx.stroke();
|
|
865
|
+
}
|
|
866
|
+
for (const n of ns) {
|
|
867
|
+
const halo = ctx.createRadialGradient(n.x,n.y,0,n.x,n.y,n.r*2.8);
|
|
868
|
+
halo.addColorStop(0,n.color+"cc"); halo.addColorStop(0.45,n.color+"33"); halo.addColorStop(1,n.color+"00");
|
|
869
|
+
ctx.fillStyle = halo; ctx.beginPath(); ctx.arc(n.x,n.y,n.r*2.8,0,Math.PI*2); ctx.fill();
|
|
870
|
+
ctx.fillStyle = n.color; ctx.beginPath(); ctx.arc(n.x,n.y,n.r,0,Math.PI*2); ctx.fill();
|
|
871
|
+
ctx.fillStyle = "rgba(255,255,255,0.9)"; ctx.beginPath(); ctx.arc(n.x-n.r*0.3, n.y-n.r*0.3, n.r*0.28, 0, Math.PI*2); ctx.fill();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
if (opts.stats) {
|
|
877
|
+
const parts = [];
|
|
878
|
+
if (c.signals) parts.push(fmtN(c.signals)+" signals");
|
|
879
|
+
if (c.days) parts.push(fmtN(c.days)+" days");
|
|
880
|
+
parts.push("100% on my machine");
|
|
881
|
+
ctx.fillStyle = "#62656D"; ctx.font = "500 22px "+FONT; ctx.fillText(parts.join(" · "), PAD, H-PAD-58);
|
|
882
|
+
}
|
|
883
|
+
ctx.fillStyle = "#9A9DA5"; ctx.font = "500 22px "+FONT; ctx.fillText("made with persnally — your own context engine", PAD, H-PAD);
|
|
884
|
+
ctx.textAlign = "right"; ctx.fillStyle = "#62656D"; ctx.fillText("persnally.com", W-PAD, H-PAD); ctx.textAlign = "left";
|
|
885
|
+
}
|
|
886
|
+
const _modal = $("shareModal");
|
|
887
|
+
function renderCard() { buildCard($("cardCanvas"), { head:$("tHead").checked, int:$("tInt").checked, voice:$("tVoice").checked, stats:$("tStats").checked }); }
|
|
888
|
+
function openShare() { renderCard(); _modal.classList.add("open"); _modal.setAttribute("aria-hidden","false"); }
|
|
889
|
+
function closeShare() { _modal.classList.remove("open"); _modal.setAttribute("aria-hidden","true"); }
|
|
890
|
+
$("shareBtn").onclick = openShare;
|
|
891
|
+
$("shareClose").onclick = closeShare;
|
|
892
|
+
_modal.addEventListener("click", (e) => { if (e.target === _modal) closeShare(); });
|
|
893
|
+
["tHead","tInt","tVoice","tStats"].forEach((id) => $(id).addEventListener("change", renderCard));
|
|
894
|
+
addEventListener("keydown", (e) => { if (e.key === "Escape" && _modal.classList.contains("open")) closeShare(); });
|
|
895
|
+
$("cardDownload").onclick = () => $("cardCanvas").toBlob((b) => {
|
|
896
|
+
if (!b) return; const u = URL.createObjectURL(b), a = document.createElement("a");
|
|
897
|
+
a.href = u; a.download = "persnally-portrait.png"; a.click(); setTimeout(() => URL.revokeObjectURL(u), 1000);
|
|
898
|
+
}, "image/png");
|
|
899
|
+
$("cardCopy").onclick = async () => {
|
|
900
|
+
const btn = $("cardCopy");
|
|
901
|
+
try {
|
|
902
|
+
const b = await new Promise((r) => $("cardCanvas").toBlob(r, "image/png"));
|
|
903
|
+
await navigator.clipboard.write([new ClipboardItem({ "image/png": b })]);
|
|
904
|
+
btn.textContent = "Copied ✓"; setTimeout(() => btn.textContent = "Copy image", 1600);
|
|
905
|
+
} catch { btn.textContent = "Copy unsupported — Download"; setTimeout(() => btn.textContent = "Copy image", 2400); }
|
|
906
|
+
};
|
|
907
|
+
|
|
670
908
|
loadAll();
|
|
671
909
|
|
|
672
910
|
/* ── demo data ── */
|
package/build/src/lifecycle.d.ts
CHANGED
|
@@ -12,3 +12,13 @@ export declare function stopDaemon(): Promise<number | null>;
|
|
|
12
12
|
export declare function autostartInstalled(): boolean;
|
|
13
13
|
export declare function installAutostart(cliPath: string, port: number): string;
|
|
14
14
|
export declare function removeAutostart(): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Reload the launchd job so the daemon restarts on the currently-installed build.
|
|
17
|
+
* `unload` then `load` — a plain `load` can't replace an already-loaded job, which
|
|
18
|
+
* is how a plist path silently drifts from the running process. Rewriting from the
|
|
19
|
+
* caller's cliPath also heals that drift. Returns the new daemon's /health once it
|
|
20
|
+
* answers, or null if it didn't come up in time.
|
|
21
|
+
*/
|
|
22
|
+
export declare function reloadAutostart(cliPath: string, port: number): Promise<{
|
|
23
|
+
version: string;
|
|
24
|
+
} | null>;
|
package/build/src/lifecycle.js
CHANGED
|
@@ -118,6 +118,27 @@ export function removeAutostart() {
|
|
|
118
118
|
rmSync(PLIST_PATH);
|
|
119
119
|
return true;
|
|
120
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Reload the launchd job so the daemon restarts on the currently-installed build.
|
|
123
|
+
* `unload` then `load` — a plain `load` can't replace an already-loaded job, which
|
|
124
|
+
* is how a plist path silently drifts from the running process. Rewriting from the
|
|
125
|
+
* caller's cliPath also heals that drift. Returns the new daemon's /health once it
|
|
126
|
+
* answers, or null if it didn't come up in time.
|
|
127
|
+
*/
|
|
128
|
+
export async function reloadAutostart(cliPath, port) {
|
|
129
|
+
removeAutostart();
|
|
130
|
+
installAutostart(cliPath, port);
|
|
131
|
+
for (let i = 0; i < 30; i++) {
|
|
132
|
+
await sleep(100);
|
|
133
|
+
try {
|
|
134
|
+
const r = await fetch(`http://127.0.0.1:${port}/health`);
|
|
135
|
+
if (r.ok)
|
|
136
|
+
return (await r.json());
|
|
137
|
+
}
|
|
138
|
+
catch { /* launchd hasn't brought it up yet */ }
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
121
142
|
function sleep(ms) {
|
|
122
143
|
return new Promise((r) => setTimeout(r, ms));
|
|
123
144
|
}
|