persnally 2.0.3 → 2.2.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 +85 -2
- package/build/src/connect.d.ts +7 -0
- package/build/src/connect.js +38 -3
- package/build/src/daemon.js +4 -0
- package/build/src/dashboard.html +593 -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 +48 -29
- package/build/src/prose.d.ts +10 -0
- package/build/src/prose.js +34 -0
- package/build/src/store.d.ts +8 -0
- package/build/src/store.js +22 -0
- package/build/src/stylometry.d.ts +21 -0
- package/build/src/stylometry.js +124 -0
- package/package.json +2 -2
package/build/src/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { existsSync, rmSync } from "node:fs";
|
|
|
8
8
|
import { homedir } from "node:os";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { applyApiKey, configPath, loadConfig, saveConfig } from "./config.js";
|
|
11
|
-
import { CLIENTS, connectAll, connectClient } from "./connect.js";
|
|
11
|
+
import { CLIENTS, connectAll, connectClient, installClaudeCodeHook } from "./connect.js";
|
|
12
12
|
import { runConsolidation } from "./consolidate.js";
|
|
13
13
|
import { chooseExtractor } from "./llm.js";
|
|
14
14
|
import { CATEGORIES, clearScope, loadScopes, setScope } from "./permissions.js";
|
|
@@ -19,6 +19,9 @@ 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 { autostartInstalled, installAutostart, LOG_FILE, removeAutostart, removePidFile, runningPid, startDetached, stopDaemon, writePidFile, } from "./lifecycle.js";
|
|
22
|
+
import { newEvent } from "./events.js";
|
|
23
|
+
import { proseLines } from "./prose.js";
|
|
24
|
+
import { analyzeVoice } from "./stylometry.js";
|
|
22
25
|
import { renderProfile, synthesizeProfile } from "./profile.js";
|
|
23
26
|
import { DEFAULT_DB_PATH, EventStore } from "./store.js";
|
|
24
27
|
const USAGE = `persnallyd ${VERSION} — so every AI finally knows you
|
|
@@ -34,8 +37,10 @@ Usage:
|
|
|
34
37
|
persnallyd import chatgpt <path> Import a ChatGPT export dir or conversations.json (needs ANTHROPIC_API_KEY)
|
|
35
38
|
persnallyd import git <path> [--author <email>] Import repo activity (offline, no LLM); path = repo or folder of repos
|
|
36
39
|
persnallyd profile Synthesize your profile from the store
|
|
40
|
+
persnallyd voice Refresh your voice fingerprint from Claude Code transcripts (offline, no LLM)
|
|
37
41
|
persnallyd consolidate Reflect now: refresh decay, add behavior patterns, re-synthesize
|
|
38
42
|
persnallyd show [topics|events|profile] Show topics (default), recent events, or the profile
|
|
43
|
+
persnallyd context [--full] Emit profile + interests for AI injection (records a context read)
|
|
39
44
|
persnallyd forget <topic> Hard-delete a topic and everything derived from it
|
|
40
45
|
persnallyd forget --all Delete all data
|
|
41
46
|
persnallyd forget --batch <id> Undo one import batch
|
|
@@ -150,9 +155,19 @@ async function main() {
|
|
|
150
155
|
}
|
|
151
156
|
store.close();
|
|
152
157
|
// 6. AI clients
|
|
153
|
-
|
|
158
|
+
const connections = connectAll();
|
|
159
|
+
for (const { client, file } of connections) {
|
|
154
160
|
console.log(file ? `✓ Connected ${client}` : `· ${client} not installed — skipped`);
|
|
155
161
|
}
|
|
162
|
+
if (connections.some((r) => r.client === "claude-code" && r.file)) {
|
|
163
|
+
try {
|
|
164
|
+
installClaudeCodeHook();
|
|
165
|
+
console.log("✓ Context hook installed (injects on every Claude Code session)");
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
console.error(`· Context hook skipped: ${e instanceof Error ? e.message : e}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
156
171
|
console.log(`\nDone${imported ? ` — ${imported} events imported` : ""}. Dashboard: http://127.0.0.1:${port}`);
|
|
157
172
|
if (process.platform === "darwin" && process.stdout.isTTY) {
|
|
158
173
|
try {
|
|
@@ -197,6 +212,15 @@ async function main() {
|
|
|
197
212
|
for (const { client, file } of results) {
|
|
198
213
|
console.log(file ? `Connected ${client} (${file})` : `${client} not installed — skipped`);
|
|
199
214
|
}
|
|
215
|
+
// Claude Code also gets a SessionStart hook so every session injects context automatically.
|
|
216
|
+
if (results.some((r) => r.client === "claude-code" && r.file)) {
|
|
217
|
+
try {
|
|
218
|
+
console.log(`Installed Claude Code context hook (${installClaudeCodeHook()})`);
|
|
219
|
+
}
|
|
220
|
+
catch (e) {
|
|
221
|
+
console.error(`Context hook not installed: ${e instanceof Error ? e.message : e}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
200
224
|
return;
|
|
201
225
|
}
|
|
202
226
|
case "config": {
|
|
@@ -282,6 +306,21 @@ async function main() {
|
|
|
282
306
|
console.log(renderProfile(profile));
|
|
283
307
|
return;
|
|
284
308
|
}
|
|
309
|
+
case "voice": {
|
|
310
|
+
// Deterministic, offline, re-runnable — refreshes the stylometry layer in place.
|
|
311
|
+
const dir = args[0] || DEFAULT_TRANSCRIPTS_DIR;
|
|
312
|
+
const { parsed } = parseClaudeCodeTranscripts(dir);
|
|
313
|
+
const corpus = parsed.conversations.flatMap((c) => proseLines(c.userMessages.join("\n")));
|
|
314
|
+
const v = analyzeVoice(corpus);
|
|
315
|
+
if (!v.signals.length)
|
|
316
|
+
return die(`Not enough prose in ${dir} to fingerprint a voice yet.`);
|
|
317
|
+
const store = new EventStore();
|
|
318
|
+
store.clearStyleByBasis("stylometry"); // replace, don't accumulate, across re-runs
|
|
319
|
+
store.append(v.signals.map((s) => newEvent("signal.style", "cli", s, { kind: "local", surface: "cli" })));
|
|
320
|
+
store.close();
|
|
321
|
+
console.log(`Voice fingerprint refreshed from ${v.prompts} prompts.\n\n${v.pack}`);
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
285
324
|
case "show": {
|
|
286
325
|
const store = new EventStore();
|
|
287
326
|
if (args[0] === "profile") {
|
|
@@ -304,6 +343,50 @@ async function main() {
|
|
|
304
343
|
store.close();
|
|
305
344
|
return;
|
|
306
345
|
}
|
|
346
|
+
case "context": {
|
|
347
|
+
// Serving path for the SessionStart hook: emit profile + interests for
|
|
348
|
+
// injection AND record the read, so hook injections count toward the
|
|
349
|
+
// context-reads metric exactly like the MCP persnally_context tool. `show`
|
|
350
|
+
// stays side-effect-free so manual inspection never inflates the metric.
|
|
351
|
+
const full = args.includes("--full");
|
|
352
|
+
const hook = args.includes("--hook");
|
|
353
|
+
const store = new EventStore();
|
|
354
|
+
const profile = store.getProfile();
|
|
355
|
+
const topics = store.topics(full ? 25 : 12);
|
|
356
|
+
if (!profile && !topics.length) {
|
|
357
|
+
store.close();
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const out = [];
|
|
361
|
+
let items = topics.length;
|
|
362
|
+
if (profile) {
|
|
363
|
+
out.push("# About the user", profile.headline, "");
|
|
364
|
+
const sections = full ? profile.sections : profile.sections.slice(0, 3);
|
|
365
|
+
items += sections.length;
|
|
366
|
+
for (const s of sections)
|
|
367
|
+
out.push(`## ${s.title}`, s.body, "");
|
|
368
|
+
}
|
|
369
|
+
if (topics.length) {
|
|
370
|
+
out.push("# Current interests (decay-weighted)");
|
|
371
|
+
for (const t of topics) {
|
|
372
|
+
out.push(`- ${t.topic} (${t.category}, ${t.dominant_intent}, weight ${t.weight.toFixed(2)})`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
// Recording must never break the injection itself (mirrors MCP recordRead).
|
|
376
|
+
try {
|
|
377
|
+
store.append([newEvent("context.read", "cli", { scope: full ? "full" : "brief", client_purpose: hook ? "session-start hook" : "cli context read", items }, { kind: "local", surface: "cli" })]);
|
|
378
|
+
}
|
|
379
|
+
catch (e) {
|
|
380
|
+
console.error("persnally: context.read not recorded:", e instanceof Error ? e.message : e);
|
|
381
|
+
}
|
|
382
|
+
store.close();
|
|
383
|
+
const rendered = out.join("\n");
|
|
384
|
+
// --hook emits the SessionStart envelope itself, so the installed hook needs no jq.
|
|
385
|
+
console.log(hook
|
|
386
|
+
? JSON.stringify({ hookSpecificOutput: { hookEventName: "SessionStart", additionalContext: rendered } })
|
|
387
|
+
: rendered);
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
307
390
|
case "forget": {
|
|
308
391
|
const store = new EventStore();
|
|
309
392
|
if (args[0] === "--all") {
|
package/build/src/connect.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ export type Client = (typeof CLIENTS)[number];
|
|
|
7
7
|
export declare function mcpServerPath(): string;
|
|
8
8
|
/** Returns the config file written, or null when the client isn't installed. */
|
|
9
9
|
export declare function connectClient(client: Client): string | null;
|
|
10
|
+
/**
|
|
11
|
+
* Installs (or upgrades) the Persnally SessionStart hook in Claude Code's user
|
|
12
|
+
* settings so every session injects the user's context. Merges into existing
|
|
13
|
+
* settings, leaves other tools' hooks untouched, and is idempotent: a prior
|
|
14
|
+
* Persnally entry (including the old `show topics` form) is replaced, not duplicated.
|
|
15
|
+
*/
|
|
16
|
+
export declare function installClaudeCodeHook(): string;
|
|
10
17
|
export declare function connectAll(): {
|
|
11
18
|
client: Client;
|
|
12
19
|
file: string | null;
|
package/build/src/connect.js
CHANGED
|
@@ -2,10 +2,17 @@
|
|
|
2
2
|
* Writes the Persnally MCP server into AI clients' configs.
|
|
3
3
|
* Only touches clients that are actually installed; merges, never clobbers.
|
|
4
4
|
*/
|
|
5
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
8
|
export const CLIENTS = ["claude-code", "claude-desktop", "cursor"];
|
|
9
|
+
/** Write JSON via temp file + rename: a crash mid-write can't corrupt the user's config. */
|
|
10
|
+
function writeJsonAtomic(file, cfg) {
|
|
11
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
12
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
13
|
+
writeFileSync(tmp, JSON.stringify(cfg, null, 2) + "\n");
|
|
14
|
+
renameSync(tmp, file);
|
|
15
|
+
}
|
|
9
16
|
function configPathFor(client) {
|
|
10
17
|
const home = homedir();
|
|
11
18
|
switch (client) {
|
|
@@ -50,8 +57,36 @@ export function connectClient(client) {
|
|
|
50
57
|
}
|
|
51
58
|
const servers = (cfg.mcpServers ??= {});
|
|
52
59
|
servers.persnally = { command: "node", args: [mcpServerPath()] };
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
writeJsonAtomic(file, cfg);
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
63
|
+
// The hook self-renders the SessionStart envelope (`context --hook`), so no jq dependency.
|
|
64
|
+
const SESSION_START_COMMAND = "persnallyd context --hook 2>/dev/null";
|
|
65
|
+
/**
|
|
66
|
+
* Installs (or upgrades) the Persnally SessionStart hook in Claude Code's user
|
|
67
|
+
* settings so every session injects the user's context. Merges into existing
|
|
68
|
+
* settings, leaves other tools' hooks untouched, and is idempotent: a prior
|
|
69
|
+
* Persnally entry (including the old `show topics` form) is replaced, not duplicated.
|
|
70
|
+
*/
|
|
71
|
+
export function installClaudeCodeHook() {
|
|
72
|
+
const file = join(homedir(), ".claude", "settings.json");
|
|
73
|
+
let cfg = {};
|
|
74
|
+
if (existsSync(file)) {
|
|
75
|
+
try {
|
|
76
|
+
cfg = JSON.parse(readFileSync(file, "utf-8"));
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw new Error(`${file} is not valid JSON — fix it, then run \`persnallyd connect claude-code\` again`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const hooks = (cfg.hooks ??= {});
|
|
83
|
+
const existing = Array.isArray(hooks.SessionStart) ? hooks.SessionStart : [];
|
|
84
|
+
const others = existing.filter((g) => !g.hooks?.some((h) => /persnall/i.test(h.command ?? "")));
|
|
85
|
+
others.push({
|
|
86
|
+
hooks: [{ type: "command", command: SESSION_START_COMMAND, timeout: 10, statusMessage: "Loading your Persnally context…" }],
|
|
87
|
+
});
|
|
88
|
+
hooks.SessionStart = others;
|
|
89
|
+
writeJsonAtomic(file, cfg);
|
|
55
90
|
return file;
|
|
56
91
|
}
|
|
57
92
|
export function connectAll() {
|
package/build/src/daemon.js
CHANGED
|
@@ -58,6 +58,10 @@ 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
|
+
}
|
|
61
65
|
if (req.method === "GET" && url.pathname === "/scopes") {
|
|
62
66
|
return json(res, 200, loadScopes());
|
|
63
67
|
}
|