persnally 2.3.1 → 2.3.2
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
CHANGED
|
@@ -13,7 +13,7 @@ import { runConsolidation } from "./consolidate.js";
|
|
|
13
13
|
import { chooseExtractor } from "./llm.js";
|
|
14
14
|
import { CATEGORIES, clearScope, loadScopes, setScope } from "./permissions.js";
|
|
15
15
|
import { alreadyImported, DENSITY_QUESTIONS, detectExports, eventsFromAnswers, isThin, markImported } from "./setup.js";
|
|
16
|
-
import { DEFAULT_PORT, startDaemon, VERSION } from "./daemon.js";
|
|
16
|
+
import { autoImportNewSessions, DEFAULT_PORT, startDaemon, VERSION } from "./daemon.js";
|
|
17
17
|
import { extractChatGPTEvents, parseChatGPTExport } from "./importers/chatgpt.js";
|
|
18
18
|
import { extractClaudeEvents, parseClaudeExport } from "./importers/claude.js";
|
|
19
19
|
import { DEFAULT_TRANSCRIPTS_DIR, extractClaudeCodeEvents, parseClaudeCodeTranscripts, } from "./importers/claude-code.js";
|
|
@@ -484,6 +484,8 @@ async function main() {
|
|
|
484
484
|
process.on("uncaughtException", (e) => { console.error("uncaughtException:", e); process.exit(1); });
|
|
485
485
|
console.error(`persnallyd v${VERSION} listening on 127.0.0.1:${port}`);
|
|
486
486
|
console.error(`Dashboard: http://127.0.0.1:${port}`);
|
|
487
|
+
// Catch up on chats since the daemon last ran; the timer takes it from here.
|
|
488
|
+
void autoImportNewSessions(store);
|
|
487
489
|
return;
|
|
488
490
|
}
|
|
489
491
|
default:
|
package/build/src/daemon.d.ts
CHANGED
|
@@ -7,3 +7,10 @@ import type { EventStore } from "./store.js";
|
|
|
7
7
|
export declare const DEFAULT_PORT = 4983;
|
|
8
8
|
export declare const VERSION: string;
|
|
9
9
|
export declare function startDaemon(store: EventStore, port?: number): http.Server;
|
|
10
|
+
/**
|
|
11
|
+
* Ingest Claude Code sessions created since the last pass — the daemon's
|
|
12
|
+
* automatic capture of new chats (no user action, no per-session hook). A
|
|
13
|
+
* key-less, Ollama-less machine has no extractor: skip rather than block.
|
|
14
|
+
* Never throws — capture must not take the daemon down.
|
|
15
|
+
*/
|
|
16
|
+
export declare function autoImportNewSessions(store: EventStore): Promise<void>;
|
package/build/src/daemon.js
CHANGED
|
@@ -8,6 +8,7 @@ import { loadConfig } from "./config.js";
|
|
|
8
8
|
import { runConsolidation, shouldRunNow } from "./consolidate.js";
|
|
9
9
|
import { allowedCategories, loadScopes } from "./permissions.js";
|
|
10
10
|
import { newEvent, validateEvent } from "./events.js";
|
|
11
|
+
import { importNewClaudeCodeSessions } from "./importers/claude-code.js";
|
|
11
12
|
import { chooseExtractor } from "./llm.js";
|
|
12
13
|
import { synthesizeProfile } from "./profile.js";
|
|
13
14
|
export const DEFAULT_PORT = 4983;
|
|
@@ -131,8 +132,9 @@ export function startDaemon(store, port = DEFAULT_PORT) {
|
|
|
131
132
|
}
|
|
132
133
|
});
|
|
133
134
|
server.listen(port, "127.0.0.1");
|
|
134
|
-
//
|
|
135
|
+
// Every 30 min: pick up new Claude Code chats, then run the once-a-day reflection.
|
|
135
136
|
const timer = setInterval(async () => {
|
|
137
|
+
await autoImportNewSessions(store);
|
|
136
138
|
const lastRun = loadConfig().last_consolidation;
|
|
137
139
|
if (!shouldRunNow(typeof lastRun === "string" ? lastRun : undefined, new Date()))
|
|
138
140
|
return;
|
|
@@ -149,6 +151,27 @@ export function startDaemon(store, port = DEFAULT_PORT) {
|
|
|
149
151
|
server.on("close", () => clearInterval(timer));
|
|
150
152
|
return server;
|
|
151
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Ingest Claude Code sessions created since the last pass — the daemon's
|
|
156
|
+
* automatic capture of new chats (no user action, no per-session hook). A
|
|
157
|
+
* key-less, Ollama-less machine has no extractor: skip rather than block.
|
|
158
|
+
* Never throws — capture must not take the daemon down.
|
|
159
|
+
*/
|
|
160
|
+
export async function autoImportNewSessions(store) {
|
|
161
|
+
try {
|
|
162
|
+
const engine = await chooseExtractor("extract").catch(() => null);
|
|
163
|
+
if (!engine)
|
|
164
|
+
return;
|
|
165
|
+
const r = await importNewClaudeCodeSessions(store, engine.extract, engine.model);
|
|
166
|
+
if (r.events) {
|
|
167
|
+
store.rebuild();
|
|
168
|
+
console.error(`auto-import: ${r.newSessions} new Claude Code session(s) → ${r.events} events`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
console.error("auto-import failed:", e instanceof Error ? e.message : e);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
152
175
|
let cachedHtml;
|
|
153
176
|
function dashboardHtml() {
|
|
154
177
|
cachedHtml ??= readFileSync(new URL("./dashboard.html", import.meta.url), "utf-8");
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* (Phase 0 finding), and available immediately with no export wait.
|
|
5
5
|
*/
|
|
6
6
|
import { type LlmExtract } from "../llm.js";
|
|
7
|
+
import type { EventStore } from "../store.js";
|
|
7
8
|
import { type ImportResult, type ParsedExport } from "./extract.js";
|
|
8
9
|
export declare const DEFAULT_TRANSCRIPTS_DIR: string;
|
|
9
10
|
export declare const DEFAULT_MAX_SESSIONS = 200;
|
|
@@ -14,3 +15,16 @@ export interface ClaudeCodeParse {
|
|
|
14
15
|
}
|
|
15
16
|
export declare function parseClaudeCodeTranscripts(root?: string, maxSessions?: number): ClaudeCodeParse;
|
|
16
17
|
export declare function extractClaudeCodeEvents(parsed: ParsedExport, extract?: LlmExtract, model?: string, file?: string): Promise<ImportResult>;
|
|
18
|
+
export interface IncrementalImport {
|
|
19
|
+
newSessions: number;
|
|
20
|
+
events: number;
|
|
21
|
+
skipped: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Import only the Claude Code sessions not already in the store — the path the
|
|
25
|
+
* daemon runs on its loop so new chats accrue without re-extracting old ones.
|
|
26
|
+
* Sessions are matched by the conversation_uuid recorded in each topic's
|
|
27
|
+
* provenance; a session that yields zero topics leaves no marker and may be
|
|
28
|
+
* retried, which is cheap and rare (a real session produces topics).
|
|
29
|
+
*/
|
|
30
|
+
export declare function importNewClaudeCodeSessions(store: EventStore, extract: LlmExtract, model?: string, root?: string): Promise<IncrementalImport>;
|
|
@@ -97,3 +97,32 @@ function humanText(content) {
|
|
|
97
97
|
export async function extractClaudeCodeEvents(parsed, extract = anthropicExtract, model = DEFAULT_EXTRACT_MODEL, file = DEFAULT_TRANSCRIPTS_DIR) {
|
|
98
98
|
return extractEvents(parsed, { source: "import:claude-code", importer: "claude-code", file }, extract, model);
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Import only the Claude Code sessions not already in the store — the path the
|
|
102
|
+
* daemon runs on its loop so new chats accrue without re-extracting old ones.
|
|
103
|
+
* Sessions are matched by the conversation_uuid recorded in each topic's
|
|
104
|
+
* provenance; a session that yields zero topics leaves no marker and may be
|
|
105
|
+
* retried, which is cheap and rare (a real session produces topics).
|
|
106
|
+
*/
|
|
107
|
+
export async function importNewClaudeCodeSessions(store, extract, model = DEFAULT_EXTRACT_MODEL, root = DEFAULT_TRANSCRIPTS_DIR) {
|
|
108
|
+
if (!existsSync(root))
|
|
109
|
+
return { newSessions: 0, events: 0, skipped: 0 };
|
|
110
|
+
const { parsed } = parseClaudeCodeTranscripts(root);
|
|
111
|
+
const seen = importedConversationUuids(store);
|
|
112
|
+
const fresh = parsed.conversations.filter((c) => !seen.has(c.uuid));
|
|
113
|
+
const skipped = parsed.conversations.length - fresh.length;
|
|
114
|
+
if (!fresh.length)
|
|
115
|
+
return { newSessions: 0, events: 0, skipped };
|
|
116
|
+
const { events } = await extractClaudeCodeEvents({ ...parsed, conversations: fresh }, extract, model, root);
|
|
117
|
+
store.append(events);
|
|
118
|
+
return { newSessions: fresh.length, events: events.length, skipped };
|
|
119
|
+
}
|
|
120
|
+
function importedConversationUuids(store) {
|
|
121
|
+
const uuids = new Set();
|
|
122
|
+
for (const e of store.query({ source: "import:claude-code", limit: 1_000_000 })) {
|
|
123
|
+
const uuid = e.provenance.conversation_uuid;
|
|
124
|
+
if (uuid)
|
|
125
|
+
uuids.add(uuid);
|
|
126
|
+
}
|
|
127
|
+
return uuids;
|
|
128
|
+
}
|
|
@@ -18,19 +18,27 @@ export async function extractEvents(parsed, opts, extract = anthropicExtract, mo
|
|
|
18
18
|
if (!convo.userMessages.length)
|
|
19
19
|
continue;
|
|
20
20
|
const joined = convo.userMessages.join("\n");
|
|
21
|
-
voiceCorpus.push(...proseLines(joined));
|
|
21
|
+
voiceCorpus.push(...proseLines(joined)); // prose feeds the deterministic voice fingerprint even if topic extraction fails
|
|
22
22
|
const text = stripNoise(joined).slice(0, MAX_CONVO_CHARS); // strip pasted paths/URLs/logs before the LLM sees it
|
|
23
23
|
if (!text)
|
|
24
24
|
continue;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
25
|
+
try {
|
|
26
|
+
const result = await extract({
|
|
27
|
+
model,
|
|
28
|
+
instruction: "Extract 1-5 topic signals from this conversation's user messages. Weight = centrality, depth = engagement level, sentiment = user's attitude toward the topic. Capture decisions and rejected options as their own signals.",
|
|
29
|
+
schema: topicsExtraction,
|
|
30
|
+
content: `Conversation title: ${convo.name}\n\nUser messages:\n${text}`,
|
|
31
|
+
});
|
|
32
|
+
const { topics } = topicsExtraction.parse(result);
|
|
33
|
+
for (const t of topics) {
|
|
34
|
+
events.push(newEvent("signal.topic", opts.source, t, { kind: "import", batch, file: opts.file, conversation_uuid: convo.uuid }, safeIso(convo.created_at)));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
// One malformed extraction (e.g. the model returns an out-of-enum value)
|
|
39
|
+
// must not abort a whole multi-conversation import. Skip it — leaving no
|
|
40
|
+
// conversation_uuid marker, so the next pass retries it — and keep the rest.
|
|
41
|
+
console.error(`extract: skipped "${convo.name}" — ${(e instanceof Error ? e.message : String(e)).split("\n")[0]}`);
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
44
|
if (parsed.memoryText.trim() || parsed.projects.length) {
|
|
@@ -38,15 +46,21 @@ export async function extractEvents(parsed, opts, extract = anthropicExtract, mo
|
|
|
38
46
|
parsed.memoryText.trim() && `Assistant's accumulated memory of the user:\n${parsed.memoryText}`,
|
|
39
47
|
parsed.projects.length && `User-created projects:\n${parsed.projects.map((p) => `- ${p.name}: ${p.description}`).join("\n")}`,
|
|
40
48
|
].filter(Boolean).join("\n\n");
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
try {
|
|
50
|
+
const result = await extract({
|
|
51
|
+
model,
|
|
52
|
+
instruction: "Extract structured assertions about this person: facts, preferences, behaviors, skills, and context. Confidence reflects how directly the source supports the claim.",
|
|
53
|
+
schema: assertionsExtraction,
|
|
54
|
+
content: context,
|
|
55
|
+
});
|
|
56
|
+
const { assertions } = assertionsExtraction.parse(result);
|
|
57
|
+
for (const a of assertions) {
|
|
58
|
+
events.push(newEvent("signal.assertion", opts.source, a, { kind: "import", batch, file: opts.file }));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (e) {
|
|
62
|
+
// A malformed assertions response shouldn't discard the topics already gathered.
|
|
63
|
+
console.error(`extract: skipped memory/projects assertions — ${(e instanceof Error ? e.message : String(e)).split("\n")[0]}`);
|
|
50
64
|
}
|
|
51
65
|
}
|
|
52
66
|
// Deterministic voice fingerprint over the user's own prose — no LLM, no tokens.
|