persnally 2.1.0 → 2.3.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 +25 -2
- package/build/src/consolidate.d.ts +1 -0
- package/build/src/consolidate.js +5 -1
- package/build/src/daemon.js +11 -1
- package/build/src/dashboard.html +603 -137
- package/build/src/events.d.ts +24 -0
- package/build/src/events.js +10 -0
- package/build/src/importers/extract.js +12 -1
- package/build/src/mcp/index.js +60 -33
- package/build/src/prose.d.ts +10 -0
- package/build/src/prose.js +34 -0
- package/build/src/setup.js +6 -1
- package/build/src/store.d.ts +23 -0
- package/build/src/store.js +77 -1
- package/build/src/stylometry.d.ts +21 -0
- package/build/src/stylometry.js +124 -0
- package/package.json +1 -1
package/build/src/cli.js
CHANGED
|
@@ -20,6 +20,8 @@ import { DEFAULT_TRANSCRIPTS_DIR, extractClaudeCodeEvents, parseClaudeCodeTransc
|
|
|
20
20
|
import { gitEvents, scanRepos } from "./importers/git.js";
|
|
21
21
|
import { autostartInstalled, installAutostart, LOG_FILE, removeAutostart, removePidFile, runningPid, startDetached, stopDaemon, writePidFile, } from "./lifecycle.js";
|
|
22
22
|
import { newEvent } from "./events.js";
|
|
23
|
+
import { proseLines } from "./prose.js";
|
|
24
|
+
import { analyzeVoice } from "./stylometry.js";
|
|
23
25
|
import { renderProfile, synthesizeProfile } from "./profile.js";
|
|
24
26
|
import { DEFAULT_DB_PATH, EventStore } from "./store.js";
|
|
25
27
|
const USAGE = `persnallyd ${VERSION} — so every AI finally knows you
|
|
@@ -35,10 +37,12 @@ Usage:
|
|
|
35
37
|
persnallyd import chatgpt <path> Import a ChatGPT export dir or conversations.json (needs ANTHROPIC_API_KEY)
|
|
36
38
|
persnallyd import git <path> [--author <email>] Import repo activity (offline, no LLM); path = repo or folder of repos
|
|
37
39
|
persnallyd profile Synthesize your profile from the store
|
|
40
|
+
persnallyd voice Refresh your voice fingerprint from Claude Code transcripts (offline, no LLM)
|
|
38
41
|
persnallyd consolidate Reflect now: refresh decay, add behavior patterns, re-synthesize
|
|
39
42
|
persnallyd show [topics|events|profile] Show topics (default), recent events, or the profile
|
|
40
43
|
persnallyd context [--full] Emit profile + interests for AI injection (records a context read)
|
|
41
44
|
persnallyd forget <topic> Hard-delete a topic and everything derived from it
|
|
45
|
+
persnallyd forget --style <dimension> <pattern> Forget a "how you write" pattern for good
|
|
42
46
|
persnallyd forget --all Delete all data
|
|
43
47
|
persnallyd forget --batch <id> Undo one import batch
|
|
44
48
|
persnallyd status Store stats and daemon health
|
|
@@ -291,7 +295,7 @@ async function main() {
|
|
|
291
295
|
const store = new EventStore();
|
|
292
296
|
const r = await runConsolidation(store, engine);
|
|
293
297
|
store.close();
|
|
294
|
-
console.log(`Consolidation: ${r.newSignals} new signal(s) since last run, ${r.assertions} behavior assertion(s) added, profile ${r.profileRefreshed ? "refreshed" : "unchanged"}.`);
|
|
298
|
+
console.log(`Consolidation: ${r.newSignals} new signal(s) since last run, ${r.assertions} behavior assertion(s) added, profile ${r.profileRefreshed ? "refreshed" : "unchanged"}, ${r.stylePruned} style signal(s) pruned.`);
|
|
295
299
|
return;
|
|
296
300
|
}
|
|
297
301
|
case "profile": {
|
|
@@ -303,6 +307,21 @@ async function main() {
|
|
|
303
307
|
console.log(renderProfile(profile));
|
|
304
308
|
return;
|
|
305
309
|
}
|
|
310
|
+
case "voice": {
|
|
311
|
+
// Deterministic, offline, re-runnable — refreshes the stylometry layer in place.
|
|
312
|
+
const dir = args[0] || DEFAULT_TRANSCRIPTS_DIR;
|
|
313
|
+
const { parsed } = parseClaudeCodeTranscripts(dir);
|
|
314
|
+
const corpus = parsed.conversations.flatMap((c) => proseLines(c.userMessages.join("\n")));
|
|
315
|
+
const v = analyzeVoice(corpus);
|
|
316
|
+
if (!v.signals.length)
|
|
317
|
+
return die(`Not enough prose in ${dir} to fingerprint a voice yet.`);
|
|
318
|
+
const store = new EventStore();
|
|
319
|
+
store.clearStyleByBasis("stylometry"); // replace, don't accumulate, across re-runs
|
|
320
|
+
store.append(v.signals.map((s) => newEvent("signal.style", "cli", s, { kind: "local", surface: "cli" })));
|
|
321
|
+
store.close();
|
|
322
|
+
console.log(`Voice fingerprint refreshed from ${v.prompts} prompts.\n\n${v.pack}`);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
306
325
|
case "show": {
|
|
307
326
|
const store = new EventStore();
|
|
308
327
|
if (args[0] === "profile") {
|
|
@@ -378,11 +397,15 @@ async function main() {
|
|
|
378
397
|
else if (args[0] === "--batch" && args[1]) {
|
|
379
398
|
console.log(`Deleted ${store.forgetBatch(args[1])} events from batch ${args[1]}.`);
|
|
380
399
|
}
|
|
400
|
+
else if (args[0] === "--style" && args[1] && args[2]) {
|
|
401
|
+
store.forgetStyle(args[1], args[2]);
|
|
402
|
+
console.log(`Forgot "${args[2]}" (${args[1]}) — won't be re-learned.`);
|
|
403
|
+
}
|
|
381
404
|
else if (args[0]) {
|
|
382
405
|
console.log(`Deleted ${store.forgetTopic(args[0])} events for "${args[0]}".`);
|
|
383
406
|
}
|
|
384
407
|
else {
|
|
385
|
-
die("usage: persnallyd forget <topic> | --all | --batch <id>");
|
|
408
|
+
die("usage: persnallyd forget <topic> | --all | --batch <id> | --style <dimension> <pattern>");
|
|
386
409
|
}
|
|
387
410
|
store.close();
|
|
388
411
|
return;
|
|
@@ -12,6 +12,7 @@ export interface ConsolidationResult {
|
|
|
12
12
|
newSignals: number;
|
|
13
13
|
assertions: number;
|
|
14
14
|
profileRefreshed: boolean;
|
|
15
|
+
stylePruned: number;
|
|
15
16
|
}
|
|
16
17
|
/** Run once per local day, at or after the consolidation hour. */
|
|
17
18
|
export declare function shouldRunNow(lastRun: string | undefined, now: Date): boolean;
|
package/build/src/consolidate.js
CHANGED
|
@@ -13,6 +13,7 @@ const ASSERTION_MIN_SIGNALS = 5;
|
|
|
13
13
|
const PROFILE_MIN_SIGNALS = 10;
|
|
14
14
|
const PROVENANCE_CAP = 100;
|
|
15
15
|
export const CONSOLIDATION_HOUR = 3; // local time
|
|
16
|
+
const STYLE_BACKLOG_CAP = 80;
|
|
16
17
|
/** Run once per local day, at or after the consolidation hour. */
|
|
17
18
|
export function shouldRunNow(lastRun, now) {
|
|
18
19
|
if (now.getHours() < CONSOLIDATION_HOUR)
|
|
@@ -34,6 +35,9 @@ export async function runConsolidation(store, engine, now = new Date()) {
|
|
|
34
35
|
.filter((e) => e.type.startsWith("signal."));
|
|
35
36
|
// Decay shifts daily even with no new events — always re-derive.
|
|
36
37
|
store.rebuild(now.getTime());
|
|
38
|
+
// Distill the voice layer: live `observed` capture has no equivalent of decay,
|
|
39
|
+
// so bound the backlog to the richest signals (capture small, store distilled).
|
|
40
|
+
const stylePruned = store.pruneStyle(STYLE_BACKLOG_CAP);
|
|
37
41
|
let assertions = [];
|
|
38
42
|
if (engine && newSignals.length >= ASSERTION_MIN_SIGNALS) {
|
|
39
43
|
const summary = newSignals
|
|
@@ -63,5 +67,5 @@ export async function runConsolidation(store, engine, now = new Date()) {
|
|
|
63
67
|
profileRefreshed = true;
|
|
64
68
|
}
|
|
65
69
|
saveConfig({ last_consolidation: now.toISOString() });
|
|
66
|
-
return { newSignals: newSignals.length, assertions: assertions.length, profileRefreshed };
|
|
70
|
+
return { newSignals: newSignals.length, assertions: assertions.length, profileRefreshed, stylePruned };
|
|
67
71
|
}
|
package/build/src/daemon.js
CHANGED
|
@@ -58,6 +58,16 @@ export function startDaemon(store, port = DEFAULT_PORT) {
|
|
|
58
58
|
const profile = store.getProfile();
|
|
59
59
|
return profile ? json(res, 200, profile) : json(res, 404, { error: "no profile synthesized yet" });
|
|
60
60
|
}
|
|
61
|
+
if (req.method === "GET" && url.pathname === "/voice") {
|
|
62
|
+
// Stylistic, not topical — served to every client (it's how you write, not what about).
|
|
63
|
+
return json(res, 200, store.voice());
|
|
64
|
+
}
|
|
65
|
+
if (req.method === "DELETE" && url.pathname.startsWith("/voice/")) {
|
|
66
|
+
const [, , dimension, pattern] = url.pathname.split("/");
|
|
67
|
+
if (!dimension || !pattern)
|
|
68
|
+
return json(res, 400, { error: "dimension and pattern required" });
|
|
69
|
+
return json(res, 200, { deleted: store.forgetStyle(dimension, decodeURIComponent(pattern)) });
|
|
70
|
+
}
|
|
61
71
|
if (req.method === "GET" && url.pathname === "/scopes") {
|
|
62
72
|
return json(res, 200, loadScopes());
|
|
63
73
|
}
|
|
@@ -129,7 +139,7 @@ export function startDaemon(store, port = DEFAULT_PORT) {
|
|
|
129
139
|
try {
|
|
130
140
|
const engine = await chooseExtractor("extract").catch(() => null);
|
|
131
141
|
const r = await runConsolidation(store, engine);
|
|
132
|
-
console.error(`consolidation: ${r.newSignals} new signals, ${r.assertions} assertions, profile ${r.profileRefreshed ? "refreshed" : "kept"}`);
|
|
142
|
+
console.error(`consolidation: ${r.newSignals} new signals, ${r.assertions} assertions, profile ${r.profileRefreshed ? "refreshed" : "kept"}, ${r.stylePruned} style signals pruned`);
|
|
133
143
|
}
|
|
134
144
|
catch (e) {
|
|
135
145
|
console.error("consolidation failed:", e instanceof Error ? e.message : e);
|