persnally 2.4.0 → 2.5.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 +22 -0
- package/build/src/daemon.js +3 -0
- package/build/src/dashboard.html +30 -5
- package/build/src/store.d.ts +21 -0
- package/build/src/store.js +47 -0
- package/package.json +1 -1
package/build/src/cli.js
CHANGED
|
@@ -47,6 +47,7 @@ Usage:
|
|
|
47
47
|
persnallyd forget --all Delete all data
|
|
48
48
|
persnallyd forget --batch <id> Undo one import batch
|
|
49
49
|
persnallyd status Store stats and daemon health
|
|
50
|
+
persnallyd activity Context-read engagement over time (retention pulse)
|
|
50
51
|
persnallyd start [--port N] Start the daemon in the background
|
|
51
52
|
persnallyd stop Stop the background daemon
|
|
52
53
|
persnallyd serve [--port N] Run the daemon in the foreground (127.0.0.1:${DEFAULT_PORT})
|
|
@@ -462,6 +463,22 @@ async function main() {
|
|
|
462
463
|
console.log(`Autostart: ${autostartInstalled() ? "installed" : "not installed"}`);
|
|
463
464
|
return;
|
|
464
465
|
}
|
|
466
|
+
case "activity": {
|
|
467
|
+
const store = new EventStore();
|
|
468
|
+
const a = store.activity();
|
|
469
|
+
store.close();
|
|
470
|
+
if (!a.firstEventAt) {
|
|
471
|
+
console.log("No activity yet — run an import or connect a client.");
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const verdict = a.retainedWeek2 === null ? `in progress (day ${a.daysSinceFirst}/14)` : a.retainedWeek2 ? "active ✓" : "inactive ✗";
|
|
475
|
+
console.log(`Onboarded ${a.daysSinceFirst}d ago · ${a.totalReads} context read(s) total`);
|
|
476
|
+
console.log(`Reads: ${a.reads7d} this week · ${a.reads30d} this month`);
|
|
477
|
+
console.log(`Active: ${a.activeDays7d}/7 days · ${a.activeDays14d}/14 days`);
|
|
478
|
+
console.log(`Week-2 retention: ${verdict}`);
|
|
479
|
+
console.log(`Last 14 days: ${sparkline(a.daily.map((d) => d.reads))}`);
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
465
482
|
case "start": {
|
|
466
483
|
const existing = runningPid();
|
|
467
484
|
if (existing)
|
|
@@ -527,6 +544,11 @@ async function main() {
|
|
|
527
544
|
process.exitCode = cmd ? 1 : 0;
|
|
528
545
|
}
|
|
529
546
|
}
|
|
547
|
+
function sparkline(values) {
|
|
548
|
+
const blocks = "▁▂▃▄▅▆▇█";
|
|
549
|
+
const max = Math.max(...values, 1);
|
|
550
|
+
return values.map((v) => (v === 0 ? "·" : blocks[Math.min(blocks.length - 1, Math.floor((v / max) * (blocks.length - 1)))])).join("");
|
|
551
|
+
}
|
|
530
552
|
function summarize(payload) {
|
|
531
553
|
const s = JSON.stringify(payload);
|
|
532
554
|
return s.length > 80 ? s.slice(0, 77) + "..." : s;
|
package/build/src/daemon.js
CHANGED
|
@@ -41,6 +41,9 @@ export function startDaemon(store, port = DEFAULT_PORT) {
|
|
|
41
41
|
if (req.method === "GET" && url.pathname === "/stats") {
|
|
42
42
|
return json(res, 200, store.stats());
|
|
43
43
|
}
|
|
44
|
+
if (req.method === "GET" && url.pathname === "/activity") {
|
|
45
|
+
return json(res, 200, store.activity());
|
|
46
|
+
}
|
|
44
47
|
if (req.method === "GET" && url.pathname === "/topics") {
|
|
45
48
|
const client = url.searchParams.get("client");
|
|
46
49
|
const allowed = client ? allowedCategories(client) : null;
|
package/build/src/dashboard.html
CHANGED
|
@@ -153,6 +153,11 @@
|
|
|
153
153
|
.read .what .l1 b { color: var(--text); } .read .what .l1 .p { color: var(--dim); }
|
|
154
154
|
.read .what .l2 { font-size: 11px; color: var(--faint); margin-top: 2px; }
|
|
155
155
|
.read .when { font-size: 11.5px; color: var(--dim); white-space: nowrap; }
|
|
156
|
+
.engage { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; font-size: 12.5px; color: var(--dim); margin: 0 0 16px; }
|
|
157
|
+
.engage b { color: var(--text); font-weight: 600; }
|
|
158
|
+
.engage .spark { display: inline-flex; align-items: flex-end; gap: 2px; height: 22px; }
|
|
159
|
+
.engage .spark i { width: 6px; min-height: 2px; border-radius: 1px; display: inline-block; }
|
|
160
|
+
.engage .rk { color: var(--faint); }
|
|
156
161
|
|
|
157
162
|
/* ── reflection ── */
|
|
158
163
|
.refl { border-left: 2px solid var(--line-2); padding: 4px 0 4px 16px; margin-bottom: 16px; }
|
|
@@ -238,6 +243,7 @@
|
|
|
238
243
|
<div class="value-num num" id="valueNum">0</div>
|
|
239
244
|
<div class="value-cap" id="valueCap"></div>
|
|
240
245
|
</div>
|
|
246
|
+
<div class="engage" id="engage"></div>
|
|
241
247
|
<div class="card" id="reads" style="padding:6px 16px"></div>
|
|
242
248
|
</section>
|
|
243
249
|
|
|
@@ -417,6 +423,22 @@ function renderReads(reads, total) {
|
|
|
417
423
|
}).join("");
|
|
418
424
|
}
|
|
419
425
|
|
|
426
|
+
/* ── engagement (retention pulse) ── */
|
|
427
|
+
function renderEngage(a) {
|
|
428
|
+
const el = $("engage");
|
|
429
|
+
if (!a || !a.firstEventAt || !a.daily) { el.innerHTML = ""; return; }
|
|
430
|
+
const max = Math.max(...a.daily.map((d) => d.reads), 1);
|
|
431
|
+
const bars = a.daily.map((d, i) => {
|
|
432
|
+
const today = i === a.daily.length - 1;
|
|
433
|
+
const h = d.reads ? Math.max(3, Math.round((d.reads / max) * 22)) : 2;
|
|
434
|
+
const c = d.reads ? (today ? "var(--text)" : "var(--dim)") : "var(--line-2)";
|
|
435
|
+
return `<i style="height:${h}px;background:${c}" title="${esc(d.date)}: ${d.reads}"></i>`;
|
|
436
|
+
}).join("");
|
|
437
|
+
const ret = a.retainedWeek2 === null ? `day ${a.daysSinceFirst}/14` : (a.retainedWeek2 ? "active ✓" : "inactive ✗");
|
|
438
|
+
el.innerHTML = `<span><b>${a.reads7d}</b> this week</span><span>active <b>${a.activeDays14d}</b> of last 14 days</span>` +
|
|
439
|
+
`<span class="spark" title="context reads, last 14 days">${bars}</span><span class="rk">week-2: ${esc(ret)}</span>`;
|
|
440
|
+
}
|
|
441
|
+
|
|
420
442
|
/* ── reflections ── */
|
|
421
443
|
function renderReflections(assertions) {
|
|
422
444
|
const behav = assertions.filter(a => a.payload && (a.payload.kind==="behavior" || a.payload.kind==="preference"));
|
|
@@ -594,16 +616,16 @@ $("vList").onclick = () => switchView("list");
|
|
|
594
616
|
|
|
595
617
|
/* ── load ── */
|
|
596
618
|
async function loadAll() {
|
|
597
|
-
let stats, profile, topics, reads, assertions, voice;
|
|
619
|
+
let stats, profile, topics, reads, assertions, voice, activity;
|
|
598
620
|
try {
|
|
599
|
-
[stats, profile, topics, reads, assertions, voice] = await Promise.all([
|
|
621
|
+
[stats, profile, topics, reads, assertions, voice, activity] = await Promise.all([
|
|
600
622
|
get("/stats"), get("/profile"), get("/topics?limit=40"),
|
|
601
|
-
get("/events?type=context.read&limit=60"), get("/events?type=signal.assertion&limit=20"), get("/voice"),
|
|
623
|
+
get("/events?type=context.read&limit=60"), get("/events?type=signal.assertion&limit=20"), get("/voice"), get("/activity"),
|
|
602
624
|
]);
|
|
603
625
|
if (!stats) throw new Error("no daemon");
|
|
604
626
|
} catch (e) {
|
|
605
627
|
DEMO = true; $("ribbon").classList.add("show");
|
|
606
|
-
({ stats, profile, topics, reads, assertions, voice } = DEMO_DATA());
|
|
628
|
+
({ stats, profile, topics, reads, assertions, voice, activity } = DEMO_DATA());
|
|
607
629
|
}
|
|
608
630
|
topics = topics||[]; reads = reads||[]; assertions = assertions||[];
|
|
609
631
|
const st = $("status");
|
|
@@ -618,6 +640,7 @@ async function loadAll() {
|
|
|
618
640
|
renderTopicList(topics);
|
|
619
641
|
renderMap(topics);
|
|
620
642
|
renderReads(reads, total);
|
|
643
|
+
renderEngage(activity);
|
|
621
644
|
renderReflections(assertions);
|
|
622
645
|
saveSnapshot(topics);
|
|
623
646
|
}
|
|
@@ -635,9 +658,10 @@ $("reflect").onclick = async () => {
|
|
|
635
658
|
};
|
|
636
659
|
setInterval(async () => {
|
|
637
660
|
if (DEMO) return;
|
|
638
|
-
const [reads, stats, voice] = await Promise.all([get("/events?type=context.read&limit=60"), get("/stats"), get("/voice")]);
|
|
661
|
+
const [reads, stats, voice, activity] = await Promise.all([get("/events?type=context.read&limit=60"), get("/stats"), get("/voice"), get("/activity")]);
|
|
639
662
|
if (reads && stats) renderReads(reads, (stats.byType && stats.byType["context.read"]) || reads.length);
|
|
640
663
|
if (voice) renderVoice(voice); // live: voice signals captured mid-session show up without a reload
|
|
664
|
+
if (activity) renderEngage(activity);
|
|
641
665
|
}, 25000);
|
|
642
666
|
|
|
643
667
|
loadAll();
|
|
@@ -683,6 +707,7 @@ function DEMO_DATA() {
|
|
|
683
707
|
{ ts:iso(86400000*2), payload:{ claim:"You consistently choose local-first, auditable tools over cloud convenience.", kind:"preference", confidence:0.86 }, provenance:{ kind:"derived" } },
|
|
684
708
|
{ ts:iso(86400000*6), payload:{ claim:"You generate new ideas fastest right when your main bet stalls.", kind:"behavior", confidence:0.71 }, provenance:{ kind:"derived" } },
|
|
685
709
|
],
|
|
710
|
+
activity: { firstEventAt: iso(86400000*214), lastReadAt: iso(3600000*2), daysSinceFirst: 214, totalReads: 1247, reads7d: 33, reads30d: 142, activeDays7d: 5, activeDays14d: 12, retainedWeek2: true, daily: [2,0,3,5,1,4,6,2,0,3,7,5,8,4].map((r,i)=>({ date:new Date(now-(13-i)*86400000).toISOString().slice(0,10), reads:r })) },
|
|
686
711
|
voice: {
|
|
687
712
|
pack: "Write like this user: terse — short, declarative sentences; leads with imperatives, minimal preamble; states things flatly; no emoji; casual register (lowercases “i”); recurring phrasing: “be 100% sure”, “industry best practices”, “validate whether it's valid or not”.",
|
|
688
713
|
items: [
|
package/build/src/store.d.ts
CHANGED
|
@@ -35,6 +35,21 @@ export interface StoredProfile {
|
|
|
35
35
|
generated_at: string;
|
|
36
36
|
model: string;
|
|
37
37
|
}
|
|
38
|
+
export interface Activity {
|
|
39
|
+
firstEventAt: string | null;
|
|
40
|
+
lastReadAt: string | null;
|
|
41
|
+
daysSinceFirst: number;
|
|
42
|
+
totalReads: number;
|
|
43
|
+
reads7d: number;
|
|
44
|
+
reads30d: number;
|
|
45
|
+
activeDays7d: number;
|
|
46
|
+
activeDays14d: number;
|
|
47
|
+
retainedWeek2: boolean | null;
|
|
48
|
+
daily: {
|
|
49
|
+
date: string;
|
|
50
|
+
reads: number;
|
|
51
|
+
}[];
|
|
52
|
+
}
|
|
38
53
|
export declare class EventStore {
|
|
39
54
|
private db;
|
|
40
55
|
constructor(path?: string);
|
|
@@ -53,6 +68,12 @@ export declare class EventStore {
|
|
|
53
68
|
first: string | null;
|
|
54
69
|
last: string | null;
|
|
55
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Engagement over time from context.read events — the retention pulse.
|
|
73
|
+
* Local/per-install only (this machine); aggregate cohort retention would
|
|
74
|
+
* require opt-in telemetry. `now` is injectable for deterministic tests.
|
|
75
|
+
*/
|
|
76
|
+
activity(now?: number): Activity;
|
|
56
77
|
topics(limit?: number): TopicRow[];
|
|
57
78
|
/** Re-derive view_topics from signal.topic events using decayed per-signal weighting. */
|
|
58
79
|
rebuild(now?: number): void;
|
package/build/src/store.js
CHANGED
|
@@ -150,6 +150,53 @@ export class EventStore {
|
|
|
150
150
|
const span = this.db.prepare("SELECT MIN(ts) first, MAX(ts) last FROM events").get();
|
|
151
151
|
return { total, byType: group("type"), bySource: group("source"), ...span };
|
|
152
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Engagement over time from context.read events — the retention pulse.
|
|
155
|
+
* Local/per-install only (this machine); aggregate cohort retention would
|
|
156
|
+
* require opt-in telemetry. `now` is injectable for deterministic tests.
|
|
157
|
+
*/
|
|
158
|
+
activity(now = Date.now()) {
|
|
159
|
+
const DAY = 86_400_000;
|
|
160
|
+
const dayKey = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
161
|
+
const reads = this.query({ type: "context.read", limit: 1_000_000 }); // ts DESC
|
|
162
|
+
const firstEventAt = this.db.prepare("SELECT MIN(ts) m FROM events").get().m;
|
|
163
|
+
const firstMs = firstEventAt ? new Date(firstEventAt).getTime() : null;
|
|
164
|
+
const daily = new Map();
|
|
165
|
+
for (let i = 13; i >= 0; i--)
|
|
166
|
+
daily.set(dayKey(now - i * DAY), 0);
|
|
167
|
+
let reads7d = 0, reads30d = 0, week2Read = false;
|
|
168
|
+
const days7 = new Set(), days14 = new Set();
|
|
169
|
+
for (const e of reads) {
|
|
170
|
+
const t = new Date(e.ts).getTime();
|
|
171
|
+
if (!Number.isFinite(t))
|
|
172
|
+
continue;
|
|
173
|
+
const age = now - t, k = dayKey(t);
|
|
174
|
+
if (age <= 7 * DAY) {
|
|
175
|
+
reads7d++;
|
|
176
|
+
days7.add(k);
|
|
177
|
+
}
|
|
178
|
+
if (age <= 14 * DAY)
|
|
179
|
+
days14.add(k);
|
|
180
|
+
if (age <= 30 * DAY)
|
|
181
|
+
reads30d++;
|
|
182
|
+
if (daily.has(k))
|
|
183
|
+
daily.set(k, (daily.get(k) ?? 0) + 1);
|
|
184
|
+
if (firstMs !== null && t >= firstMs + 7 * DAY && t < firstMs + 14 * DAY)
|
|
185
|
+
week2Read = true;
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
firstEventAt,
|
|
189
|
+
lastReadAt: reads.length ? reads[0].ts : null,
|
|
190
|
+
daysSinceFirst: firstMs !== null ? Math.max(0, Math.floor((now - firstMs) / DAY)) : 0,
|
|
191
|
+
totalReads: reads.length,
|
|
192
|
+
reads7d,
|
|
193
|
+
reads30d,
|
|
194
|
+
activeDays7d: days7.size,
|
|
195
|
+
activeDays14d: days14.size,
|
|
196
|
+
retainedWeek2: firstMs !== null && now >= firstMs + 14 * DAY ? week2Read : null,
|
|
197
|
+
daily: [...daily.entries()].map(([date, r]) => ({ date, reads: r })),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
153
200
|
topics(limit = 50) {
|
|
154
201
|
const rows = this.db
|
|
155
202
|
.prepare("SELECT * FROM view_topics ORDER BY weight DESC LIMIT ?")
|