persnally 2.0.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.
Files changed (50) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +96 -0
  3. package/build/src/cli.d.ts +6 -0
  4. package/build/src/cli.js +404 -0
  5. package/build/src/config.d.ts +10 -0
  6. package/build/src/config.js +42 -0
  7. package/build/src/connect.d.ts +13 -0
  8. package/build/src/connect.js +51 -0
  9. package/build/src/consolidate.d.ts +18 -0
  10. package/build/src/consolidate.js +67 -0
  11. package/build/src/daemon.d.ts +9 -0
  12. package/build/src/daemon.js +167 -0
  13. package/build/src/dashboard.html +181 -0
  14. package/build/src/decay.d.ts +19 -0
  15. package/build/src/decay.js +33 -0
  16. package/build/src/events.d.ts +180 -0
  17. package/build/src/events.js +133 -0
  18. package/build/src/importers/chatgpt.d.ts +9 -0
  19. package/build/src/importers/chatgpt.js +34 -0
  20. package/build/src/importers/claude-code.d.ts +16 -0
  21. package/build/src/importers/claude-code.js +99 -0
  22. package/build/src/importers/claude.d.ts +8 -0
  23. package/build/src/importers/claude.js +52 -0
  24. package/build/src/importers/extract.d.ts +31 -0
  25. package/build/src/importers/extract.js +53 -0
  26. package/build/src/importers/git.d.ts +23 -0
  27. package/build/src/importers/git.js +123 -0
  28. package/build/src/lifecycle.d.ts +14 -0
  29. package/build/src/lifecycle.js +119 -0
  30. package/build/src/llm.d.ts +25 -0
  31. package/build/src/llm.js +76 -0
  32. package/build/src/mcp/daemon-client.d.ts +11 -0
  33. package/build/src/mcp/daemon-client.js +42 -0
  34. package/build/src/mcp/index.d.ts +10 -0
  35. package/build/src/mcp/index.js +158 -0
  36. package/build/src/mcp/migrate-v1.d.ts +6 -0
  37. package/build/src/mcp/migrate-v1.js +48 -0
  38. package/build/src/mcp/telemetry.d.ts +8 -0
  39. package/build/src/mcp/telemetry.js +29 -0
  40. package/build/src/paths.d.ts +2 -0
  41. package/build/src/paths.js +4 -0
  42. package/build/src/permissions.d.ts +14 -0
  43. package/build/src/permissions.js +33 -0
  44. package/build/src/profile.d.ts +22 -0
  45. package/build/src/profile.js +62 -0
  46. package/build/src/setup.d.ts +23 -0
  47. package/build/src/setup.js +111 -0
  48. package/build/src/store.d.ts +62 -0
  49. package/build/src/store.js +233 -0
  50. package/package.json +56 -0
package/LICENSE ADDED
@@ -0,0 +1,110 @@
1
+ # Functional Source License, Version 1.1, MIT Future License
2
+
3
+ ## Abbreviation
4
+
5
+ FSL-1.1-MIT
6
+
7
+ ## Notice
8
+
9
+ Copyright 2026 Persnally
10
+
11
+ ## Terms and Conditions
12
+
13
+ ### Licensor ("We")
14
+
15
+ The party offering the Software under these Terms and Conditions.
16
+
17
+ ### The Software
18
+
19
+ The "Software" is each version of the software that we make available under
20
+ these Terms and Conditions, as indicated by our inclusion of these Terms and
21
+ Conditions with the Software.
22
+
23
+ ### License Grant
24
+
25
+ Subject to your compliance with this License Grant and the Patents,
26
+ Redistribution and Trademark clauses below, we hereby grant you the right to
27
+ use, copy, modify, create derivative works, publicly perform, publicly display
28
+ and redistribute the Software for any Permitted Purpose identified below.
29
+
30
+ ### Permitted Purpose
31
+
32
+ A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
33
+ means making the Software available to others in a commercial product or
34
+ service that:
35
+
36
+ 1. substitutes for the Software;
37
+
38
+ 2. substitutes for any other product or service we offer using the Software
39
+ that exists as of the date we make the Software available; or
40
+
41
+ 3. offers the same or substantially similar functionality as the Software.
42
+
43
+ Permitted Purposes specifically include using the Software:
44
+
45
+ 1. for your internal use and access;
46
+
47
+ 2. for non-commercial education;
48
+
49
+ 3. for non-commercial research; and
50
+
51
+ 4. in connection with professional services that you provide to a licensee
52
+ using the Software in accordance with these Terms and Conditions.
53
+
54
+ ### Patents
55
+
56
+ To the extent your use for a Permitted Purpose would necessarily infringe our
57
+ patents, the license grant above includes a license under our patents. If you
58
+ make a claim against any party that the Software infringes or contributes to
59
+ the infringement of any patent, then your patent license to the Software ends
60
+ immediately.
61
+
62
+ ### Redistribution
63
+
64
+ The Terms and Conditions apply to all copies, modifications and derivatives of
65
+ the Software.
66
+
67
+ If you redistribute any copies, modifications or derivatives of the Software,
68
+ you must include a copy of or a link to these Terms and Conditions and not
69
+ remove any copyright notices provided in or with the Software.
70
+
71
+ ### Disclaimer
72
+
73
+ THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
74
+ IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
75
+ PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
76
+
77
+ IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
78
+ SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
79
+ EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
80
+
81
+ ### Trademarks
82
+
83
+ Except for displaying the License Details and identifying us as the origin of
84
+ the Software, you have no right under these Terms and Conditions to use our
85
+ trademarks, trade names, service marks or product names.
86
+
87
+ ## Grant of Future License
88
+
89
+ We hereby irrevocably grant you an additional license to use the Software under
90
+ the MIT license that is effective on the second anniversary of the date we make
91
+ the Software available. On or after that date, you may use the Software under
92
+ the MIT license, in which case the following will apply:
93
+
94
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
95
+ this software and associated documentation files (the "Software"), to deal in
96
+ the Software without restriction, including without limitation the rights to
97
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
98
+ of the Software, and to permit persons to whom the Software is furnished to do
99
+ so, subject to the following conditions:
100
+
101
+ The above copyright notice and this permission notice shall be included in all
102
+ copies or substantial portions of the Software.
103
+
104
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
105
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
106
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
107
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
108
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
109
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
110
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Persnally
2
+
3
+ [![CI](https://github.com/sidpan2011/persnally/actions/workflows/ci.yml/badge.svg)](https://github.com/sidpan2011/persnally/actions/workflows/ci.yml)
4
+
5
+ **So every AI finally knows you.**
6
+
7
+ Persnally is a local-first personal context engine. It learns who you are from your AI activity — your Claude and ChatGPT history, your code — and serves that context to every AI tool you use, so they stop treating you like a stranger.
8
+
9
+ Your context lives on your machine. Not in our cloud, not in any model vendor's silo. You can read every byte, see why it believes each thing, and delete any of it.
10
+
11
+ > **The giants build the intelligence. Persnally makes it yours.**
12
+
13
+ ## Why
14
+
15
+ Every AI you use is brilliant and amnesiac. ChatGPT doesn't know what you told Claude. Your coding agent doesn't know your stack or your tolerances. Each one relearns you from zero, every session — or interrupts you to ask.
16
+
17
+ The fix isn't a better model. It's a layer underneath all of them that holds *you*: your interests, your projects, how you decide, what you're avoiding. The model vendors won't build this — they can't share your context with each other, and their business is keeping you inside their walls. So it has to be neutral, and it has to be yours.
18
+
19
+ ## The five-minute wow
20
+
21
+ ```bash
22
+ npm install -g persnally
23
+ persnallyd start # the local daemon
24
+ persnallyd import claude ~/Downloads/<your-claude-export>
25
+ persnallyd import git ~/Projects # offline, no API needed
26
+ persnallyd profile # synthesize who you are
27
+ open http://127.0.0.1:4983 # see it, with evidence for every claim
28
+ ```
29
+
30
+ Export your data ([claude.ai](https://claude.com) / [chatgpt.com](https://chatgpt.com) → Settings → Data export), point Persnally at it, and read a description of yourself that's sharper than your own bio — every sentence traceable to the conversations it came from.
31
+
32
+ ## How it works
33
+
34
+ ```
35
+ Your AI clients (Claude, Cursor, agents…) Importers (claude · chatgpt · git)
36
+ │ MCP: context out, signals in │ your history → events
37
+ ▼ ▼
38
+ ┌──────────────────────── persnallyd (local daemon) ────────────────────────┐
39
+ │ Append-only event log (SQLite) — the single source of truth │
40
+ │ → extractors (decay-weighted interests, assertions, skills) │
41
+ │ → derived views (always re-derivable, every claim cites its events) │
42
+ └───────────────────────────────┬───────────────────────────────────────────┘
43
+ loopback only · dashboard · CLI · MCP server
44
+ ```
45
+
46
+ - **Event-sourced.** Everything is an append-only event; the profile and interest graph are *derived views* you can rebuild or delete at will.
47
+ - **Provenance-complete.** Every claim in your profile links to the exact events behind it — the dashboard's "why does it think this?" is a real answer, not a guess.
48
+ - **Truly deletable.** `persnallyd forget <topic>` hard-deletes the events *and* everything derived from them. No tombstones, no residue.
49
+ - **Deterministic reads.** Serving context to an AI never calls a model — it's instant, free, and works offline. Models run only at import and synthesis.
50
+
51
+ ## Make your AI tools use it
52
+
53
+ Add the MCP server to any client (Claude Desktop, Cursor, Claude Code). It exposes four tools backed by the daemon:
54
+
55
+ | Tool | What it does |
56
+ |------|-------------|
57
+ | `persnally_context` | Returns who you are + current interests, for the AI to use |
58
+ | `persnally_track` | Records signals from the conversation (topics, decisions, preferences) |
59
+ | `persnally_interests` | Shows you your own tracked profile |
60
+ | `persnally_forget` | Deletes a topic, or wipes everything |
61
+
62
+ ```jsonc
63
+ // e.g. Claude Desktop — claude_desktop_config.json
64
+ { "mcpServers": { "persnally": { "command": "persnally-mcp" } } }
65
+ ```
66
+
67
+ ## Your data, your rules
68
+
69
+ - **Local-first.** State lives in `~/.persnally`. Nothing leaves your machine except, at import/synthesis, the text you choose to send to your own LLM for extraction (bring your own key).
70
+ - **Structured signals only.** Raw conversations are never stored — only `{ topic, weight, intent, sentiment, category, … }` and provenance pointers.
71
+ - **Inspectable & deletable.** The dashboard shows everything; the delete button means it.
72
+ - **Source-available.** Read the engine, audit the claims, run it yourself.
73
+
74
+ ## CLI
75
+
76
+ ```
77
+ persnallyd start | stop | status # daemon lifecycle
78
+ persnallyd autostart [--remove] # run at login (macOS)
79
+ persnallyd import claude|chatgpt|git <path>
80
+ persnallyd profile # synthesize the profile
81
+ persnallyd show [topics|events|profile]
82
+ persnallyd forget <topic> | --all | --batch <id>
83
+ persnallyd config set-key <sk-ant-…> # key for the background daemon
84
+ ```
85
+
86
+ ## Status
87
+
88
+ Early and moving fast — see [ROADMAP.md](./ROADMAP.md). Today: import from Claude/ChatGPT/git, a decay-weighted interest graph, an evidence-linked profile, a local dashboard, and the MCP layer that serves it all. Next: cross-tool context everywhere, then a behavior model that can answer *what would I do here?*
89
+
90
+ ## License
91
+
92
+ [FSL-1.1-MIT](./LICENSE) — read it, audit it, run it, fork it for anything except reselling it as a competing service. Every release automatically becomes plain MIT two years after it ships. The [event schema](./docs/EVENT_SCHEMA.md) and MCP interface are an open spec — build against them freely.
93
+
94
+ ## Contributing
95
+
96
+ Issues and PRs welcome. The codebase holds itself to a high bar — see [CONTRIBUTING.md](./CONTRIBUTING.md).
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * persnallyd CLI — the developer's window into the daemon.
4
+ * Merges into the `persnally` npm identity at Phase 1 launch.
5
+ */
6
+ export {};
@@ -0,0 +1,404 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * persnallyd CLI — the developer's window into the daemon.
4
+ * Merges into the `persnally` npm identity at Phase 1 launch.
5
+ */
6
+ import { execFileSync } from "node:child_process";
7
+ import { existsSync, rmSync } from "node:fs";
8
+ import { homedir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { applyApiKey, configPath, loadConfig, saveConfig } from "./config.js";
11
+ import { CLIENTS, connectAll, connectClient } from "./connect.js";
12
+ import { runConsolidation } from "./consolidate.js";
13
+ import { chooseExtractor } from "./llm.js";
14
+ import { CATEGORIES, clearScope, loadScopes, setScope } from "./permissions.js";
15
+ import { alreadyImported, DENSITY_QUESTIONS, detectExports, eventsFromAnswers, isThin, markImported } from "./setup.js";
16
+ import { DEFAULT_PORT, startDaemon, VERSION } from "./daemon.js";
17
+ import { extractChatGPTEvents, parseChatGPTExport } from "./importers/chatgpt.js";
18
+ import { extractClaudeEvents, parseClaudeExport } from "./importers/claude.js";
19
+ import { DEFAULT_TRANSCRIPTS_DIR, extractClaudeCodeEvents, parseClaudeCodeTranscripts, } from "./importers/claude-code.js";
20
+ import { gitEvents, scanRepos } from "./importers/git.js";
21
+ import { autostartInstalled, installAutostart, LOG_FILE, removeAutostart, removePidFile, runningPid, startDetached, stopDaemon, writePidFile, } from "./lifecycle.js";
22
+ import { renderProfile, synthesizeProfile } from "./profile.js";
23
+ import { DEFAULT_DB_PATH, EventStore } from "./store.js";
24
+ const USAGE = `persnallyd ${VERSION} — so every AI finally knows you
25
+
26
+ Usage:
27
+ persnallyd setup One command: find exports, import, synthesize, connect, open
28
+ persnallyd connect [client|--all] Add Persnally to claude-code | claude-desktop | cursor
29
+ persnallyd scope <client> <categories|--clear> Limit what a client can read (e.g. scope cursor technology,career)
30
+ persnallyd scope Show all client scopes
31
+ persnallyd init Create the local store (~/.persnally/persnally.db)
32
+ persnallyd import claude <dir> Import a Claude data export (needs ANTHROPIC_API_KEY)
33
+ persnallyd import claude-code [dir] Import Claude Code session transcripts (default ~/.claude/projects)
34
+ persnallyd import chatgpt <path> Import a ChatGPT export dir or conversations.json (needs ANTHROPIC_API_KEY)
35
+ persnallyd import git <path> [--author <email>] Import repo activity (offline, no LLM); path = repo or folder of repos
36
+ persnallyd profile Synthesize your profile from the store
37
+ persnallyd consolidate Reflect now: refresh decay, add behavior patterns, re-synthesize
38
+ persnallyd show [topics|events|profile] Show topics (default), recent events, or the profile
39
+ persnallyd forget <topic> Hard-delete a topic and everything derived from it
40
+ persnallyd forget --all Delete all data
41
+ persnallyd forget --batch <id> Undo one import batch
42
+ persnallyd status Store stats and daemon health
43
+ persnallyd start [--port N] Start the daemon in the background
44
+ persnallyd stop Stop the background daemon
45
+ persnallyd serve [--port N] Run the daemon in the foreground (127.0.0.1:${DEFAULT_PORT})
46
+ persnallyd autostart [--remove] Start the daemon at login and keep it alive (macOS)
47
+ persnallyd config set-key <key> Store the Anthropic API key (owner-only file) for the daemon
48
+ persnallyd config Show config (key masked)
49
+ `;
50
+ function parsePort(args) {
51
+ const i = args.indexOf("--port");
52
+ return i > -1 && args[i + 1] ? Number(args[i + 1]) : DEFAULT_PORT;
53
+ }
54
+ async function main() {
55
+ const [cmd, ...args] = process.argv.slice(2);
56
+ applyApiKey();
57
+ switch (cmd) {
58
+ case "setup": {
59
+ const port = parsePort(args);
60
+ console.log("Persnally setup — so every AI finally knows you.\n");
61
+ // 1. Extraction engine (optional — git-only works without one)
62
+ let engine = null;
63
+ try {
64
+ engine = await chooseExtractor("extract");
65
+ console.log(`✓ Extraction engine: ${engine.label}`);
66
+ }
67
+ catch {
68
+ console.log("· No extraction engine (no API key, no Ollama) — conversation imports skipped, git still works.");
69
+ }
70
+ // 2. Daemon
71
+ if (!runningPid()) {
72
+ await startDetached(process.argv[1], port);
73
+ console.log(`✓ Daemon started (http://127.0.0.1:${port})`);
74
+ }
75
+ else {
76
+ console.log("✓ Daemon already running");
77
+ }
78
+ // 3. Conversation exports from ~/Downloads (zipped or unzipped)
79
+ const store = new EventStore();
80
+ let imported = 0;
81
+ for (const found of detectExports()) {
82
+ if (alreadyImported(found.origin)) {
83
+ console.log(`· Skipping ${found.origin} (already imported)`);
84
+ }
85
+ else if (engine) {
86
+ console.log(`→ Importing ${found.kind} export: ${found.origin}`);
87
+ const parsed = found.kind === "claude" ? parseClaudeExport(found.path) : parseChatGPTExport(found.path);
88
+ const result = found.kind === "claude"
89
+ ? await extractClaudeEvents(parsed, engine.extract, engine.model)
90
+ : await extractChatGPTEvents(parsed, engine.extract, engine.model);
91
+ store.append(result.events);
92
+ markImported(found.origin);
93
+ imported += result.events.length;
94
+ console.log(` ✓ ${result.events.length} events`);
95
+ }
96
+ if (found.cleanup)
97
+ rmSync(found.cleanup, { recursive: true, force: true });
98
+ }
99
+ // 3b. Claude Code transcripts — local, no export wait. Capped at the 50 most
100
+ // recent sessions so setup stays fast; full history via `import claude-code`.
101
+ if (engine && existsSync(DEFAULT_TRANSCRIPTS_DIR) && !alreadyImported(DEFAULT_TRANSCRIPTS_DIR)) {
102
+ const { parsed, sessionsFound, sessionsDropped } = parseClaudeCodeTranscripts(DEFAULT_TRANSCRIPTS_DIR, 50);
103
+ if (parsed.conversations.length) {
104
+ console.log(`→ Importing Claude Code transcripts: ${parsed.conversations.length} session(s)` +
105
+ (sessionsDropped ? ` (most recent of ${sessionsFound} — full history: persnallyd import claude-code)` : ""));
106
+ const result = await extractClaudeCodeEvents(parsed, engine.extract, engine.model);
107
+ store.append(result.events);
108
+ markImported(DEFAULT_TRANSCRIPTS_DIR);
109
+ imported += result.events.length;
110
+ console.log(` ✓ ${result.events.length} events`);
111
+ }
112
+ }
113
+ // 4. Git activity from ~/Projects
114
+ const projects = join(homedir(), "Projects");
115
+ if (existsSync(projects) && !alreadyImported(projects)) {
116
+ const summaries = scanRepos(projects);
117
+ if (summaries.length) {
118
+ const { events } = gitEvents(summaries);
119
+ store.append(events);
120
+ markImported(projects);
121
+ imported += events.length;
122
+ console.log(`✓ Imported ${summaries.length} git repo(s) from ~/Projects (${events.length} events, fully offline)`);
123
+ }
124
+ }
125
+ store.rebuild();
126
+ // 4b. Density floor — if everything is still thin, two questions beat an empty mirror
127
+ const signalCount = store.stats().byType["signal.topic"] ?? 0;
128
+ if (isThin(signalCount) && process.stdin.isTTY) {
129
+ console.log("\nYour history is light — two quick questions so Persnally starts with something real:");
130
+ const { createInterface } = await import("node:readline/promises");
131
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
132
+ const answers = [];
133
+ for (const q of DENSITY_QUESTIONS)
134
+ answers.push(await rl.question(` ${q}\n > `));
135
+ rl.close();
136
+ const seeds = await eventsFromAnswers(answers, engine);
137
+ if (seeds.length) {
138
+ store.append(seeds);
139
+ store.rebuild();
140
+ imported += seeds.length;
141
+ console.log(` ✓ Seeded ${seeds.length} signal(s) from your answers`);
142
+ }
143
+ }
144
+ // 5. Profile
145
+ if (engine && store.stats().total > 0) {
146
+ console.log("→ Synthesizing your profile…");
147
+ const profileEngine = await chooseExtractor("profile");
148
+ await synthesizeProfile(store, profileEngine.extract, profileEngine.model);
149
+ console.log(" ✓ Profile ready");
150
+ }
151
+ store.close();
152
+ // 6. AI clients
153
+ for (const { client, file } of connectAll()) {
154
+ console.log(file ? `✓ Connected ${client}` : `· ${client} not installed — skipped`);
155
+ }
156
+ console.log(`\nDone${imported ? ` — ${imported} events imported` : ""}. Dashboard: http://127.0.0.1:${port}`);
157
+ if (process.platform === "darwin" && process.stdout.isTTY) {
158
+ try {
159
+ execFileSync("open", [`http://127.0.0.1:${port}`]);
160
+ }
161
+ catch { /* non-fatal */ }
162
+ }
163
+ return;
164
+ }
165
+ case "scope": {
166
+ const [client, spec] = args;
167
+ if (!client) {
168
+ const scopes = loadScopes();
169
+ const entries = Object.entries(scopes);
170
+ if (!entries.length) {
171
+ console.log("No client scopes — every connected client sees everything.");
172
+ return;
173
+ }
174
+ for (const [c, cats] of entries)
175
+ console.log(`${c}: ${cats.join(", ")}`);
176
+ return;
177
+ }
178
+ if (!spec)
179
+ return die("usage: persnallyd scope <client> <cat1,cat2|--clear>");
180
+ if (spec === "--clear") {
181
+ console.log(clearScope(client) ? `Cleared scope for ${client} — it now sees everything.` : `${client} had no scope.`);
182
+ return;
183
+ }
184
+ const cats = spec.split(",").map((c) => c.trim()).filter(Boolean);
185
+ const invalid = cats.filter((c) => !CATEGORIES.includes(c));
186
+ if (invalid.length)
187
+ return die(`unknown categor${invalid.length > 1 ? "ies" : "y"}: ${invalid.join(", ")}\nvalid: ${CATEGORIES.join(", ")}`);
188
+ setScope(client, cats);
189
+ console.log(`${client} can now read only: ${cats.join(", ")}. Restart that client to apply.`);
190
+ return;
191
+ }
192
+ case "connect": {
193
+ const target = args[0] === "--all" || !args[0] ? null : args[0];
194
+ if (target && !CLIENTS.includes(target))
195
+ return die(`unknown client — use ${CLIENTS.join(" | ")} | --all`);
196
+ const results = target ? [{ client: target, file: connectClient(target) }] : connectAll();
197
+ for (const { client, file } of results) {
198
+ console.log(file ? `Connected ${client} (${file})` : `${client} not installed — skipped`);
199
+ }
200
+ return;
201
+ }
202
+ case "config": {
203
+ if (args[0] === "set-key") {
204
+ if (!args[1]?.startsWith("sk-ant-"))
205
+ return die("expected an Anthropic key (sk-ant-...)");
206
+ saveConfig({ anthropic_api_key: args[1] });
207
+ console.log(`Key saved to ${configPath()} (mode 600). Restart the daemon to apply: persnallyd stop`);
208
+ return;
209
+ }
210
+ const cfg = loadConfig();
211
+ const key = typeof cfg.anthropic_api_key === "string" ? cfg.anthropic_api_key : "";
212
+ console.log(`Config: ${configPath()}`);
213
+ console.log(`anthropic_api_key: ${key ? key.slice(0, 12) + "…" + key.slice(-4) : "(not set)"}`);
214
+ return;
215
+ }
216
+ case "init": {
217
+ const store = new EventStore();
218
+ store.close();
219
+ console.log(`Initialized ${DEFAULT_DB_PATH}`);
220
+ return;
221
+ }
222
+ case "import": {
223
+ const [kind, path] = args;
224
+ const usage = "usage: persnallyd import claude|claude-code|chatgpt|git <path>";
225
+ if (!kind)
226
+ return die(usage);
227
+ let events, batch;
228
+ if (kind === "claude-code") {
229
+ const engine = await chooseExtractor("extract");
230
+ const root = path ?? DEFAULT_TRANSCRIPTS_DIR;
231
+ const { parsed, sessionsFound, sessionsDropped } = parseClaudeCodeTranscripts(root);
232
+ if (!parsed.conversations.length)
233
+ return die(`No usable sessions found at ${root}`);
234
+ console.error(`Found ${sessionsFound} session(s)${sessionsDropped ? ` — importing the ${parsed.conversations.length} most recent` : ""}. ` +
235
+ `Extracting with ${engine.label}...`);
236
+ ({ events, batch } = await extractClaudeCodeEvents(parsed, engine.extract, engine.model, root));
237
+ }
238
+ else if (!path) {
239
+ return die(usage);
240
+ }
241
+ else if (kind === "git") {
242
+ const authorIdx = args.indexOf("--author");
243
+ const summaries = scanRepos(path, authorIdx > -1 ? args[authorIdx + 1] : undefined);
244
+ if (!summaries.length)
245
+ return die(`No git repos with your commits found at ${path}`);
246
+ console.error(`Found ${summaries.length} repo(s): ${summaries.map((s) => `${s.repo} (${s.commits} commits)`).join(", ")}`);
247
+ ({ events, batch } = gitEvents(summaries));
248
+ }
249
+ else if (kind === "claude" || kind === "chatgpt") {
250
+ const engine = await chooseExtractor("extract");
251
+ const parsed = kind === "claude" ? parseClaudeExport(path) : parseChatGPTExport(path);
252
+ console.error(`Parsed ${parsed.conversations.length} conversations. Extracting with ${engine.label}...`);
253
+ ({ events, batch } = kind === "claude"
254
+ ? await extractClaudeEvents(parsed, engine.extract, engine.model)
255
+ : await extractChatGPTEvents(parsed, engine.extract, engine.model));
256
+ }
257
+ else {
258
+ return die(`unknown import source "${kind}" — use claude, claude-code, chatgpt, or git`);
259
+ }
260
+ const store = new EventStore();
261
+ store.append(events);
262
+ store.rebuild();
263
+ store.close();
264
+ console.log(`Imported ${events.length} events (batch ${batch}).`);
265
+ console.log(`Undo with: persnallyd forget --batch ${batch}`);
266
+ return;
267
+ }
268
+ case "consolidate": {
269
+ const engine = await chooseExtractor("extract").catch(() => null);
270
+ const store = new EventStore();
271
+ const r = await runConsolidation(store, engine);
272
+ store.close();
273
+ console.log(`Consolidation: ${r.newSignals} new signal(s) since last run, ${r.assertions} behavior assertion(s) added, profile ${r.profileRefreshed ? "refreshed" : "unchanged"}.`);
274
+ return;
275
+ }
276
+ case "profile": {
277
+ const engine = await chooseExtractor("profile");
278
+ const store = new EventStore();
279
+ console.error(`Synthesizing profile with ${engine.label}...`);
280
+ const profile = await synthesizeProfile(store, engine.extract, engine.model);
281
+ store.close();
282
+ console.log(renderProfile(profile));
283
+ return;
284
+ }
285
+ case "show": {
286
+ const store = new EventStore();
287
+ if (args[0] === "profile") {
288
+ const p = store.getProfile();
289
+ console.log(p ? renderProfile(p) : "No profile yet. Run: persnallyd profile");
290
+ }
291
+ else if (args[0] === "events") {
292
+ for (const e of store.query({ limit: 20 })) {
293
+ console.log(`${e.ts} ${e.type.padEnd(18)} ${e.source.padEnd(16)} ${summarize(e.payload)}`);
294
+ }
295
+ }
296
+ else {
297
+ const topics = store.topics(25);
298
+ if (!topics.length)
299
+ console.log("No topics yet. Run an import or connect an MCP client.");
300
+ for (const t of topics) {
301
+ console.log(`${t.weight.toFixed(2).padStart(6)} ${t.topic} (${t.category}, ${t.signals} signals)`);
302
+ }
303
+ }
304
+ store.close();
305
+ return;
306
+ }
307
+ case "forget": {
308
+ const store = new EventStore();
309
+ if (args[0] === "--all") {
310
+ store.forgetAll();
311
+ console.log("All data deleted.");
312
+ }
313
+ else if (args[0] === "--batch" && args[1]) {
314
+ console.log(`Deleted ${store.forgetBatch(args[1])} events from batch ${args[1]}.`);
315
+ }
316
+ else if (args[0]) {
317
+ console.log(`Deleted ${store.forgetTopic(args[0])} events for "${args[0]}".`);
318
+ }
319
+ else {
320
+ die("usage: persnallyd forget <topic> | --all | --batch <id>");
321
+ }
322
+ store.close();
323
+ return;
324
+ }
325
+ case "status": {
326
+ const store = new EventStore();
327
+ const s = store.stats();
328
+ store.close();
329
+ console.log(`Store: ${DEFAULT_DB_PATH}`);
330
+ console.log(`Events: ${s.total} (${s.first ?? "—"} → ${s.last ?? "—"})`);
331
+ for (const [t, n] of Object.entries(s.byType))
332
+ console.log(` ${t}: ${n}`);
333
+ const pid = runningPid();
334
+ console.log(pid ? `Daemon: running (pid ${pid})` : "Daemon: not running");
335
+ console.log(`Autostart: ${autostartInstalled() ? "installed" : "not installed"}`);
336
+ return;
337
+ }
338
+ case "start": {
339
+ const existing = runningPid();
340
+ if (existing)
341
+ return die(`daemon already running (pid ${existing})`);
342
+ const pid = await startDetached(process.argv[1], parsePort(args));
343
+ console.log(`persnallyd started (pid ${pid}). Dashboard: http://127.0.0.1:${parsePort(args)}`);
344
+ console.log(`Logs: ${LOG_FILE}`);
345
+ return;
346
+ }
347
+ case "stop": {
348
+ if (autostartInstalled()) {
349
+ console.error("Note: autostart is installed — launchd will restart the daemon. Use `persnallyd autostart --remove` to stop it permanently.");
350
+ }
351
+ const pid = await stopDaemon();
352
+ console.log(pid ? `Stopped daemon (pid ${pid}).` : "Daemon was not running.");
353
+ return;
354
+ }
355
+ case "autostart": {
356
+ if (args[0] === "--remove") {
357
+ console.log(removeAutostart() ? "Autostart removed; daemon stopped." : "Autostart was not installed.");
358
+ return;
359
+ }
360
+ // A running daemon holds the pidfile and would put launchd in a retry loop — hand over first.
361
+ const stopped = await stopDaemon();
362
+ if (stopped)
363
+ console.log(`Stopped existing daemon (pid ${stopped}) — launchd takes over.`);
364
+ const plist = installAutostart(process.argv[1], parsePort(args));
365
+ console.log(`Autostart installed (${plist}). The daemon now runs at login and restarts if it exits.`);
366
+ return;
367
+ }
368
+ case "serve": {
369
+ const existing = runningPid();
370
+ if (existing)
371
+ return die(`daemon already running (pid ${existing}) — stop it first`);
372
+ const port = parsePort(args);
373
+ const store = new EventStore();
374
+ const server = startDaemon(store, port);
375
+ server.on("error", (e) => {
376
+ die(e.code === "EADDRINUSE" ? `port ${port} is already in use` : e.message);
377
+ });
378
+ writePidFile();
379
+ const shutdown = () => {
380
+ server.close();
381
+ store.close();
382
+ removePidFile();
383
+ process.exit(0);
384
+ };
385
+ process.on("SIGTERM", shutdown);
386
+ process.on("SIGINT", shutdown);
387
+ console.error(`persnallyd v${VERSION} listening on 127.0.0.1:${port}`);
388
+ console.error(`Dashboard: http://127.0.0.1:${port}`);
389
+ return;
390
+ }
391
+ default:
392
+ console.log(USAGE);
393
+ process.exitCode = cmd ? 1 : 0;
394
+ }
395
+ }
396
+ function summarize(payload) {
397
+ const s = JSON.stringify(payload);
398
+ return s.length > 80 ? s.slice(0, 77) + "..." : s;
399
+ }
400
+ function die(msg) {
401
+ console.error(msg);
402
+ process.exit(1);
403
+ }
404
+ main().catch((e) => die(e instanceof Error ? e.message : String(e)));
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Daemon config at ~/.persnally/config.json — most importantly the Anthropic
3
+ * key, so the launchd-run daemon (no shell env) can synthesize. Unknown fields
4
+ * are preserved (the file predates v2). Saved with owner-only permissions.
5
+ */
6
+ export declare function loadConfig(): Record<string, unknown>;
7
+ export declare function saveConfig(updates: Record<string, unknown>): void;
8
+ /** Env wins over config; sets process.env so the Anthropic SDK picks it up. */
9
+ export declare function applyApiKey(): boolean;
10
+ export declare function configPath(): string;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Daemon config at ~/.persnally/config.json — most importantly the Anthropic
3
+ * key, so the launchd-run daemon (no shell env) can synthesize. Unknown fields
4
+ * are preserved (the file predates v2). Saved with owner-only permissions.
5
+ */
6
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
7
+ import { dirname, join } from "node:path";
8
+ import { DATA_DIR } from "./paths.js";
9
+ // Resolved at call time so PERSNALLY_DIR overrides work in-process (tests), not just for subprocesses.
10
+ function configFile() {
11
+ return join(process.env.PERSNALLY_DIR ?? DATA_DIR, "config.json");
12
+ }
13
+ export function loadConfig() {
14
+ try {
15
+ return JSON.parse(readFileSync(configFile(), "utf-8"));
16
+ }
17
+ catch {
18
+ return {};
19
+ }
20
+ }
21
+ export function saveConfig(updates) {
22
+ const file = configFile();
23
+ mkdirSync(dirname(file), { recursive: true });
24
+ const merged = { ...loadConfig(), ...updates };
25
+ writeFileSync(file, JSON.stringify(merged, null, 2) + "\n");
26
+ chmodSync(file, 0o600);
27
+ }
28
+ /** Env wins over config; sets process.env so the Anthropic SDK picks it up. */
29
+ export function applyApiKey() {
30
+ if (process.env.ANTHROPIC_API_KEY)
31
+ return true;
32
+ const key = loadConfig().anthropic_api_key;
33
+ if (typeof key === "string" && key.startsWith("sk-ant-")) {
34
+ process.env.ANTHROPIC_API_KEY = key;
35
+ return true;
36
+ }
37
+ return false;
38
+ }
39
+ export function configPath() {
40
+ const file = configFile();
41
+ return existsSync(file) ? file : `${file} (not created yet)`;
42
+ }