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.
- package/LICENSE +110 -0
- package/README.md +96 -0
- package/build/src/cli.d.ts +6 -0
- package/build/src/cli.js +404 -0
- package/build/src/config.d.ts +10 -0
- package/build/src/config.js +42 -0
- package/build/src/connect.d.ts +13 -0
- package/build/src/connect.js +51 -0
- package/build/src/consolidate.d.ts +18 -0
- package/build/src/consolidate.js +67 -0
- package/build/src/daemon.d.ts +9 -0
- package/build/src/daemon.js +167 -0
- package/build/src/dashboard.html +181 -0
- package/build/src/decay.d.ts +19 -0
- package/build/src/decay.js +33 -0
- package/build/src/events.d.ts +180 -0
- package/build/src/events.js +133 -0
- package/build/src/importers/chatgpt.d.ts +9 -0
- package/build/src/importers/chatgpt.js +34 -0
- package/build/src/importers/claude-code.d.ts +16 -0
- package/build/src/importers/claude-code.js +99 -0
- package/build/src/importers/claude.d.ts +8 -0
- package/build/src/importers/claude.js +52 -0
- package/build/src/importers/extract.d.ts +31 -0
- package/build/src/importers/extract.js +53 -0
- package/build/src/importers/git.d.ts +23 -0
- package/build/src/importers/git.js +123 -0
- package/build/src/lifecycle.d.ts +14 -0
- package/build/src/lifecycle.js +119 -0
- package/build/src/llm.d.ts +25 -0
- package/build/src/llm.js +76 -0
- package/build/src/mcp/daemon-client.d.ts +11 -0
- package/build/src/mcp/daemon-client.js +42 -0
- package/build/src/mcp/index.d.ts +10 -0
- package/build/src/mcp/index.js +158 -0
- package/build/src/mcp/migrate-v1.d.ts +6 -0
- package/build/src/mcp/migrate-v1.js +48 -0
- package/build/src/mcp/telemetry.d.ts +8 -0
- package/build/src/mcp/telemetry.js +29 -0
- package/build/src/paths.d.ts +2 -0
- package/build/src/paths.js +4 -0
- package/build/src/permissions.d.ts +14 -0
- package/build/src/permissions.js +33 -0
- package/build/src/profile.d.ts +22 -0
- package/build/src/profile.js +62 -0
- package/build/src/setup.d.ts +23 -0
- package/build/src/setup.js +111 -0
- package/build/src/store.d.ts +62 -0
- package/build/src/store.js +233 -0
- package/package.json +56 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event types and validation — the code form of docs/EVENT_SCHEMA.md.
|
|
3
|
+
* The type set is closed: unknown types fail ingestion loudly.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export declare const SCHEMA_VERSION = 1;
|
|
7
|
+
export declare const PAYLOAD_SCHEMAS: {
|
|
8
|
+
readonly "signal.topic": z.ZodObject<{
|
|
9
|
+
topic: z.ZodString;
|
|
10
|
+
weight: z.ZodNumber;
|
|
11
|
+
intent: z.ZodEnum<{
|
|
12
|
+
learning: "learning";
|
|
13
|
+
building: "building";
|
|
14
|
+
researching: "researching";
|
|
15
|
+
deciding: "deciding";
|
|
16
|
+
discussing: "discussing";
|
|
17
|
+
debugging: "debugging";
|
|
18
|
+
}>;
|
|
19
|
+
sentiment: z.ZodEnum<{
|
|
20
|
+
positive: "positive";
|
|
21
|
+
negative: "negative";
|
|
22
|
+
neutral: "neutral";
|
|
23
|
+
}>;
|
|
24
|
+
depth: z.ZodEnum<{
|
|
25
|
+
mention: "mention";
|
|
26
|
+
moderate: "moderate";
|
|
27
|
+
deep: "deep";
|
|
28
|
+
}>;
|
|
29
|
+
category: z.ZodEnum<{
|
|
30
|
+
technology: "technology";
|
|
31
|
+
business: "business";
|
|
32
|
+
finance: "finance";
|
|
33
|
+
career: "career";
|
|
34
|
+
health: "health";
|
|
35
|
+
science: "science";
|
|
36
|
+
creative: "creative";
|
|
37
|
+
education: "education";
|
|
38
|
+
lifestyle: "lifestyle";
|
|
39
|
+
news: "news";
|
|
40
|
+
other: "other";
|
|
41
|
+
}>;
|
|
42
|
+
entities: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
43
|
+
}, z.core.$strip>;
|
|
44
|
+
readonly "signal.assertion": z.ZodObject<{
|
|
45
|
+
claim: z.ZodString;
|
|
46
|
+
kind: z.ZodEnum<{
|
|
47
|
+
fact: "fact";
|
|
48
|
+
preference: "preference";
|
|
49
|
+
behavior: "behavior";
|
|
50
|
+
skill: "skill";
|
|
51
|
+
context: "context";
|
|
52
|
+
}>;
|
|
53
|
+
confidence: z.ZodNumber;
|
|
54
|
+
evidence: z.ZodString;
|
|
55
|
+
}, z.core.$strip>;
|
|
56
|
+
readonly "signal.skill": z.ZodObject<{
|
|
57
|
+
skill: z.ZodString;
|
|
58
|
+
domain: z.ZodString;
|
|
59
|
+
proficiency: z.ZodNumber;
|
|
60
|
+
basis: z.ZodString;
|
|
61
|
+
}, z.core.$strip>;
|
|
62
|
+
readonly "context.read": z.ZodObject<{
|
|
63
|
+
scope: z.ZodString;
|
|
64
|
+
client_purpose: z.ZodString;
|
|
65
|
+
items: z.ZodNumber;
|
|
66
|
+
}, z.core.$strip>;
|
|
67
|
+
readonly "agent.question": z.ZodObject<{
|
|
68
|
+
question: z.ZodString;
|
|
69
|
+
asker: z.ZodString;
|
|
70
|
+
}, z.core.$strip>;
|
|
71
|
+
readonly "agent.answer": z.ZodObject<{
|
|
72
|
+
question_id: z.ZodString;
|
|
73
|
+
answer: z.ZodString;
|
|
74
|
+
confidence: z.ZodNumber;
|
|
75
|
+
deferred: z.ZodBoolean;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
readonly "feedback.signal": z.ZodObject<{
|
|
78
|
+
subject_id: z.ZodString;
|
|
79
|
+
verdict: z.ZodEnum<{
|
|
80
|
+
approved: "approved";
|
|
81
|
+
edited: "edited";
|
|
82
|
+
vetoed: "vetoed";
|
|
83
|
+
}>;
|
|
84
|
+
}, z.core.$strip>;
|
|
85
|
+
readonly "user.correction": z.ZodObject<{
|
|
86
|
+
target_id: z.ZodString;
|
|
87
|
+
action: z.ZodEnum<{
|
|
88
|
+
delete: "delete";
|
|
89
|
+
edit: "edit";
|
|
90
|
+
contradict: "contradict";
|
|
91
|
+
}>;
|
|
92
|
+
reason: z.ZodDefault<z.ZodString>;
|
|
93
|
+
}, z.core.$strip>;
|
|
94
|
+
readonly "system.import": z.ZodObject<{
|
|
95
|
+
importer: z.ZodString;
|
|
96
|
+
batch: z.ZodString;
|
|
97
|
+
events: z.ZodNumber;
|
|
98
|
+
source_span: z.ZodOptional<z.ZodTuple<[z.ZodString, z.ZodString], null>>;
|
|
99
|
+
}, z.core.$strip>;
|
|
100
|
+
};
|
|
101
|
+
export type EventType = keyof typeof PAYLOAD_SCHEMAS;
|
|
102
|
+
export declare const EVENT_TYPES: EventType[];
|
|
103
|
+
export declare const provenanceSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
104
|
+
kind: z.ZodLiteral<"mcp">;
|
|
105
|
+
client: z.ZodString;
|
|
106
|
+
session: z.ZodOptional<z.ZodString>;
|
|
107
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
108
|
+
kind: z.ZodLiteral<"import">;
|
|
109
|
+
batch: z.ZodString;
|
|
110
|
+
file: z.ZodString;
|
|
111
|
+
conversation_uuid: z.ZodOptional<z.ZodString>;
|
|
112
|
+
message_uuid: z.ZodOptional<z.ZodString>;
|
|
113
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
114
|
+
kind: z.ZodLiteral<"git">;
|
|
115
|
+
repo: z.ZodString;
|
|
116
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
117
|
+
batch: z.ZodOptional<z.ZodString>;
|
|
118
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
119
|
+
kind: z.ZodLiteral<"derived">;
|
|
120
|
+
from: z.ZodArray<z.ZodString>;
|
|
121
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
122
|
+
kind: z.ZodLiteral<"local">;
|
|
123
|
+
surface: z.ZodEnum<{
|
|
124
|
+
cli: "cli";
|
|
125
|
+
dashboard: "dashboard";
|
|
126
|
+
}>;
|
|
127
|
+
}, z.core.$strip>], "kind">;
|
|
128
|
+
export declare const eventSchema: z.ZodObject<{
|
|
129
|
+
id: z.ZodString;
|
|
130
|
+
ts: z.ZodString;
|
|
131
|
+
recorded_at: z.ZodString;
|
|
132
|
+
source: z.ZodString;
|
|
133
|
+
type: z.ZodEnum<{
|
|
134
|
+
"signal.topic": "signal.topic";
|
|
135
|
+
"signal.assertion": "signal.assertion";
|
|
136
|
+
"signal.skill": "signal.skill";
|
|
137
|
+
"context.read": "context.read";
|
|
138
|
+
"agent.question": "agent.question";
|
|
139
|
+
"agent.answer": "agent.answer";
|
|
140
|
+
"feedback.signal": "feedback.signal";
|
|
141
|
+
"user.correction": "user.correction";
|
|
142
|
+
"system.import": "system.import";
|
|
143
|
+
}>;
|
|
144
|
+
payload: z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
145
|
+
provenance: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
146
|
+
kind: z.ZodLiteral<"mcp">;
|
|
147
|
+
client: z.ZodString;
|
|
148
|
+
session: z.ZodOptional<z.ZodString>;
|
|
149
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
150
|
+
kind: z.ZodLiteral<"import">;
|
|
151
|
+
batch: z.ZodString;
|
|
152
|
+
file: z.ZodString;
|
|
153
|
+
conversation_uuid: z.ZodOptional<z.ZodString>;
|
|
154
|
+
message_uuid: z.ZodOptional<z.ZodString>;
|
|
155
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
156
|
+
kind: z.ZodLiteral<"git">;
|
|
157
|
+
repo: z.ZodString;
|
|
158
|
+
ref: z.ZodOptional<z.ZodString>;
|
|
159
|
+
batch: z.ZodOptional<z.ZodString>;
|
|
160
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
161
|
+
kind: z.ZodLiteral<"derived">;
|
|
162
|
+
from: z.ZodArray<z.ZodString>;
|
|
163
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
164
|
+
kind: z.ZodLiteral<"local">;
|
|
165
|
+
surface: z.ZodEnum<{
|
|
166
|
+
cli: "cli";
|
|
167
|
+
dashboard: "dashboard";
|
|
168
|
+
}>;
|
|
169
|
+
}, z.core.$strip>], "kind">;
|
|
170
|
+
schema_ver: z.ZodLiteral<1>;
|
|
171
|
+
}, z.core.$strip>;
|
|
172
|
+
export type PersnallyEvent = z.infer<typeof eventSchema>;
|
|
173
|
+
export type Provenance = z.infer<typeof provenanceSchema>;
|
|
174
|
+
/** Validates envelope and type-specific payload. Throws ZodError on violation. */
|
|
175
|
+
export declare function validateEvent(raw: unknown): PersnallyEvent;
|
|
176
|
+
export declare function newEvent(type: EventType, source: string, payload: Record<string, unknown>, provenance: Provenance, occurredAt?: string): PersnallyEvent;
|
|
177
|
+
/** UUIDv7: 48-bit ms timestamp + random — time-ordered ids that merge cleanly across devices. */
|
|
178
|
+
export declare function uuidv7(): string;
|
|
179
|
+
/** Topic normalization, carried over from v1's interest engine (proven merge rules). */
|
|
180
|
+
export declare function normalizeTopic(topic: string): string;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event types and validation — the code form of docs/EVENT_SCHEMA.md.
|
|
3
|
+
* The type set is closed: unknown types fail ingestion loudly.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
export const SCHEMA_VERSION = 1;
|
|
7
|
+
const intent = z.enum(["learning", "building", "researching", "deciding", "discussing", "debugging"]);
|
|
8
|
+
const sentiment = z.enum(["positive", "negative", "neutral"]);
|
|
9
|
+
const depth = z.enum(["mention", "moderate", "deep"]);
|
|
10
|
+
const category = z.enum([
|
|
11
|
+
"technology", "business", "finance", "career", "health",
|
|
12
|
+
"science", "creative", "education", "lifestyle", "news", "other",
|
|
13
|
+
]);
|
|
14
|
+
export const PAYLOAD_SCHEMAS = {
|
|
15
|
+
"signal.topic": z.object({
|
|
16
|
+
topic: z.string().min(1),
|
|
17
|
+
weight: z.number().min(0).max(1),
|
|
18
|
+
intent,
|
|
19
|
+
sentiment,
|
|
20
|
+
depth,
|
|
21
|
+
category,
|
|
22
|
+
entities: z.array(z.string()).default([]),
|
|
23
|
+
}),
|
|
24
|
+
"signal.assertion": z.object({
|
|
25
|
+
claim: z.string().min(1),
|
|
26
|
+
kind: z.enum(["fact", "preference", "behavior", "skill", "context"]),
|
|
27
|
+
confidence: z.number().min(0).max(1),
|
|
28
|
+
evidence: z.string(),
|
|
29
|
+
}),
|
|
30
|
+
"signal.skill": z.object({
|
|
31
|
+
skill: z.string().min(1),
|
|
32
|
+
domain: z.string(),
|
|
33
|
+
proficiency: z.number().min(0).max(1),
|
|
34
|
+
basis: z.string(),
|
|
35
|
+
}),
|
|
36
|
+
"context.read": z.object({
|
|
37
|
+
scope: z.string(),
|
|
38
|
+
client_purpose: z.string(),
|
|
39
|
+
items: z.number().int().nonnegative(),
|
|
40
|
+
}),
|
|
41
|
+
"agent.question": z.object({
|
|
42
|
+
question: z.string().min(1),
|
|
43
|
+
asker: z.string(),
|
|
44
|
+
}),
|
|
45
|
+
"agent.answer": z.object({
|
|
46
|
+
question_id: z.string(),
|
|
47
|
+
answer: z.string(),
|
|
48
|
+
confidence: z.number().min(0).max(1),
|
|
49
|
+
deferred: z.boolean(),
|
|
50
|
+
}),
|
|
51
|
+
"feedback.signal": z.object({
|
|
52
|
+
subject_id: z.string(),
|
|
53
|
+
verdict: z.enum(["approved", "edited", "vetoed"]),
|
|
54
|
+
}),
|
|
55
|
+
"user.correction": z.object({
|
|
56
|
+
target_id: z.string(),
|
|
57
|
+
action: z.enum(["delete", "edit", "contradict"]),
|
|
58
|
+
reason: z.string().default(""),
|
|
59
|
+
}),
|
|
60
|
+
"system.import": z.object({
|
|
61
|
+
importer: z.string(),
|
|
62
|
+
batch: z.string(),
|
|
63
|
+
events: z.number().int().nonnegative(),
|
|
64
|
+
source_span: z.tuple([z.string(), z.string()]).optional(),
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
export const EVENT_TYPES = Object.keys(PAYLOAD_SCHEMAS);
|
|
68
|
+
export const provenanceSchema = z.discriminatedUnion("kind", [
|
|
69
|
+
z.object({ kind: z.literal("mcp"), client: z.string(), session: z.string().optional() }),
|
|
70
|
+
z.object({
|
|
71
|
+
kind: z.literal("import"),
|
|
72
|
+
batch: z.string(),
|
|
73
|
+
file: z.string(),
|
|
74
|
+
conversation_uuid: z.string().optional(),
|
|
75
|
+
message_uuid: z.string().optional(),
|
|
76
|
+
}),
|
|
77
|
+
z.object({ kind: z.literal("git"), repo: z.string(), ref: z.string().optional(), batch: z.string().optional() }),
|
|
78
|
+
z.object({ kind: z.literal("derived"), from: z.array(z.string()).min(1) }),
|
|
79
|
+
z.object({ kind: z.literal("local"), surface: z.enum(["cli", "dashboard"]) }),
|
|
80
|
+
]);
|
|
81
|
+
const sourcePattern = /^(mcp:[a-z0-9._-]+|import:(claude-code|claude|chatgpt|git)|cli|dashboard|system)$/;
|
|
82
|
+
export const eventSchema = z.object({
|
|
83
|
+
id: z.string().uuid(),
|
|
84
|
+
ts: z.string().datetime({ offset: true }),
|
|
85
|
+
recorded_at: z.string().datetime({ offset: true }),
|
|
86
|
+
source: z.string().regex(sourcePattern),
|
|
87
|
+
type: z.enum(EVENT_TYPES),
|
|
88
|
+
payload: z.record(z.string(), z.unknown()),
|
|
89
|
+
provenance: provenanceSchema,
|
|
90
|
+
schema_ver: z.literal(SCHEMA_VERSION),
|
|
91
|
+
});
|
|
92
|
+
/** Validates envelope and type-specific payload. Throws ZodError on violation. */
|
|
93
|
+
export function validateEvent(raw) {
|
|
94
|
+
const event = eventSchema.parse(raw);
|
|
95
|
+
PAYLOAD_SCHEMAS[event.type].parse(event.payload);
|
|
96
|
+
return event;
|
|
97
|
+
}
|
|
98
|
+
export function newEvent(type, source, payload, provenance, occurredAt) {
|
|
99
|
+
const now = new Date().toISOString();
|
|
100
|
+
return validateEvent({
|
|
101
|
+
id: uuidv7(),
|
|
102
|
+
ts: occurredAt ?? now,
|
|
103
|
+
recorded_at: now,
|
|
104
|
+
source,
|
|
105
|
+
type,
|
|
106
|
+
payload,
|
|
107
|
+
provenance,
|
|
108
|
+
schema_ver: SCHEMA_VERSION,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/** UUIDv7: 48-bit ms timestamp + random — time-ordered ids that merge cleanly across devices. */
|
|
112
|
+
export function uuidv7() {
|
|
113
|
+
const bytes = new Uint8Array(16);
|
|
114
|
+
crypto.getRandomValues(bytes);
|
|
115
|
+
const ts = BigInt(Date.now());
|
|
116
|
+
for (let i = 0; i < 6; i++)
|
|
117
|
+
bytes[i] = Number((ts >> BigInt(8 * (5 - i))) & 0xffn);
|
|
118
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x70;
|
|
119
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
120
|
+
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
121
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
122
|
+
}
|
|
123
|
+
/** Topic normalization, carried over from v1's interest engine (proven merge rules). */
|
|
124
|
+
export function normalizeTopic(topic) {
|
|
125
|
+
let key = topic.toLowerCase().trim();
|
|
126
|
+
for (const fw of ["node", "next", "vue", "react", "three", "angular", "nuxt", "ember"]) {
|
|
127
|
+
key = key.replace(new RegExp(`\\b${fw}[.\\s]?js\\b`, "g"), `${fw}js`);
|
|
128
|
+
}
|
|
129
|
+
key = key.replace(/\.js\b/g, "js").replace(/\.ts\b/g, "ts");
|
|
130
|
+
key = key.replace(/\bc\+\+/g, "cpp").replace(/\bc#/g, "csharp").replace(/\bf#/g, "fsharp");
|
|
131
|
+
key = key.replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "_");
|
|
132
|
+
return key;
|
|
133
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT data-export importer. The export's conversations.json stores each
|
|
3
|
+
* conversation as a node tree ("mapping"); user text lives in author.role
|
|
4
|
+
* === "user" nodes with content.parts. Multimodal parts are skipped.
|
|
5
|
+
*/
|
|
6
|
+
import { type LlmExtract } from "../llm.js";
|
|
7
|
+
import { type ImportResult, type ParsedExport } from "./extract.js";
|
|
8
|
+
export declare function parseChatGPTExport(path: string): ParsedExport;
|
|
9
|
+
export declare function extractChatGPTEvents(parsed: ParsedExport, extract?: LlmExtract, model?: string): Promise<ImportResult>;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ChatGPT data-export importer. The export's conversations.json stores each
|
|
3
|
+
* conversation as a node tree ("mapping"); user text lives in author.role
|
|
4
|
+
* === "user" nodes with content.parts. Multimodal parts are skipped.
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync, statSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { anthropicExtract, DEFAULT_EXTRACT_MODEL } from "../llm.js";
|
|
9
|
+
import { extractEvents } from "./extract.js";
|
|
10
|
+
export function parseChatGPTExport(path) {
|
|
11
|
+
const file = statSync(path).isDirectory() ? join(path, "conversations.json") : path;
|
|
12
|
+
if (!existsSync(file))
|
|
13
|
+
throw new Error(`No conversations.json at ${path}`);
|
|
14
|
+
const raw = JSON.parse(readFileSync(file, "utf-8"));
|
|
15
|
+
const conversations = raw.map((c) => {
|
|
16
|
+
const nodes = Object.values(c.mapping ?? {})
|
|
17
|
+
.filter((n) => n.message?.author?.role === "user")
|
|
18
|
+
.sort((a, b) => (a.message?.create_time ?? 0) - (b.message?.create_time ?? 0));
|
|
19
|
+
const userMessages = nodes
|
|
20
|
+
.flatMap((n) => n.message?.content?.parts ?? [])
|
|
21
|
+
.filter((p) => typeof p === "string" && p.trim().length > 0);
|
|
22
|
+
return {
|
|
23
|
+
uuid: String(c.conversation_id ?? c.id ?? ""),
|
|
24
|
+
name: String(c.title ?? ""),
|
|
25
|
+
summary: "",
|
|
26
|
+
created_at: c.create_time ? new Date(c.create_time * 1000).toISOString() : new Date().toISOString(),
|
|
27
|
+
userMessages,
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
return { conversations, memoryText: "", projects: [] };
|
|
31
|
+
}
|
|
32
|
+
export async function extractChatGPTEvents(parsed, extract = anthropicExtract, model = DEFAULT_EXTRACT_MODEL) {
|
|
33
|
+
return extractEvents(parsed, { source: "import:chatgpt", importer: "chatgpt", file: "conversations.json" }, extract, model);
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code transcript importer — ~/.claude/projects holds JSONL session
|
|
3
|
+
* logs locally: for developers a richer corpus than the claude.ai export
|
|
4
|
+
* (Phase 0 finding), and available immediately with no export wait.
|
|
5
|
+
*/
|
|
6
|
+
import { type LlmExtract } from "../llm.js";
|
|
7
|
+
import { type ImportResult, type ParsedExport } from "./extract.js";
|
|
8
|
+
export declare const DEFAULT_TRANSCRIPTS_DIR: string;
|
|
9
|
+
export declare const DEFAULT_MAX_SESSIONS = 200;
|
|
10
|
+
export interface ClaudeCodeParse {
|
|
11
|
+
parsed: ParsedExport;
|
|
12
|
+
sessionsFound: number;
|
|
13
|
+
sessionsDropped: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function parseClaudeCodeTranscripts(root?: string, maxSessions?: number): ClaudeCodeParse;
|
|
16
|
+
export declare function extractClaudeCodeEvents(parsed: ParsedExport, extract?: LlmExtract, model?: string, file?: string): Promise<ImportResult>;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code transcript importer — ~/.claude/projects holds JSONL session
|
|
3
|
+
* logs locally: for developers a richer corpus than the claude.ai export
|
|
4
|
+
* (Phase 0 finding), and available immediately with no export wait.
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { basename, join } from "node:path";
|
|
9
|
+
import { anthropicExtract, DEFAULT_EXTRACT_MODEL } from "../llm.js";
|
|
10
|
+
import { extractEvents } from "./extract.js";
|
|
11
|
+
export const DEFAULT_TRANSCRIPTS_DIR = join(homedir(), ".claude", "projects");
|
|
12
|
+
export const DEFAULT_MAX_SESSIONS = 200;
|
|
13
|
+
const MIN_USER_MESSAGES = 2;
|
|
14
|
+
export function parseClaudeCodeTranscripts(root = DEFAULT_TRANSCRIPTS_DIR, maxSessions = DEFAULT_MAX_SESSIONS) {
|
|
15
|
+
if (!existsSync(root))
|
|
16
|
+
throw new Error(`No Claude Code transcripts at ${root}`);
|
|
17
|
+
const sessions = [];
|
|
18
|
+
for (const project of readdirSync(root, { withFileTypes: true })) {
|
|
19
|
+
if (!project.isDirectory())
|
|
20
|
+
continue;
|
|
21
|
+
const dir = join(root, project.name);
|
|
22
|
+
for (const file of readdirSync(dir).filter((f) => f.endsWith(".jsonl"))) {
|
|
23
|
+
const session = parseSession(join(dir, file));
|
|
24
|
+
if (session && session.userMessages.length >= MIN_USER_MESSAGES)
|
|
25
|
+
sessions.push(session);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
sessions.sort((a, b) => b.created_at.localeCompare(a.created_at));
|
|
29
|
+
const kept = sessions.slice(0, maxSessions);
|
|
30
|
+
return {
|
|
31
|
+
parsed: { conversations: kept, memoryText: "", projects: [] },
|
|
32
|
+
sessionsFound: sessions.length,
|
|
33
|
+
sessionsDropped: sessions.length - kept.length,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function parseSession(path) {
|
|
37
|
+
let title = "";
|
|
38
|
+
let cwd = "";
|
|
39
|
+
let firstTs = "";
|
|
40
|
+
let sessionId = "";
|
|
41
|
+
const userMessages = [];
|
|
42
|
+
for (const line of readFileSync(path, "utf-8").split("\n")) {
|
|
43
|
+
if (!line.trim())
|
|
44
|
+
continue;
|
|
45
|
+
let entry;
|
|
46
|
+
// A crashed session can leave a truncated tail line — skip it, keep the rest.
|
|
47
|
+
try {
|
|
48
|
+
entry = JSON.parse(line);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (entry.type === "ai-title" && typeof entry.aiTitle === "string") {
|
|
54
|
+
title = entry.aiTitle;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (entry.type !== "user" || entry.isMeta || entry.isSidechain || "toolUseResult" in entry)
|
|
58
|
+
continue;
|
|
59
|
+
if (!firstTs && typeof entry.timestamp === "string")
|
|
60
|
+
firstTs = entry.timestamp;
|
|
61
|
+
if (!cwd && typeof entry.cwd === "string")
|
|
62
|
+
cwd = entry.cwd;
|
|
63
|
+
if (!sessionId && typeof entry.sessionId === "string")
|
|
64
|
+
sessionId = entry.sessionId;
|
|
65
|
+
const text = humanText(entry.message?.content);
|
|
66
|
+
if (text)
|
|
67
|
+
userMessages.push(text);
|
|
68
|
+
}
|
|
69
|
+
if (!userMessages.length)
|
|
70
|
+
return null;
|
|
71
|
+
return {
|
|
72
|
+
uuid: sessionId || basename(path, ".jsonl"),
|
|
73
|
+
name: title || (cwd ? `Claude Code session in ${basename(cwd)}` : "Claude Code session"),
|
|
74
|
+
summary: "",
|
|
75
|
+
created_at: firstTs || new Date().toISOString(),
|
|
76
|
+
userMessages,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Human prompt text only — drop slash-command palettes, interrupts, and injected reminders. */
|
|
80
|
+
function humanText(content) {
|
|
81
|
+
const parts = typeof content === "string"
|
|
82
|
+
? [content]
|
|
83
|
+
: Array.isArray(content)
|
|
84
|
+
? content
|
|
85
|
+
.filter((b) => !!b && typeof b === "object" && b.type === "text")
|
|
86
|
+
.map((b) => b.text)
|
|
87
|
+
: [];
|
|
88
|
+
const text = parts
|
|
89
|
+
.join("\n")
|
|
90
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "")
|
|
91
|
+
.trim();
|
|
92
|
+
if (text.startsWith("<command-") || text.startsWith("<local-command") || text.startsWith("[Request interrupted")) {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
return text;
|
|
96
|
+
}
|
|
97
|
+
export async function extractClaudeCodeEvents(parsed, extract = anthropicExtract, model = DEFAULT_EXTRACT_MODEL, file = DEFAULT_TRANSCRIPTS_DIR) {
|
|
98
|
+
return extractEvents(parsed, { source: "import:claude-code", importer: "claude-code", file }, extract, model);
|
|
99
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude data-export importer. Phase 0 finding: memories.json and projects
|
|
3
|
+
* are the highest-signal-per-byte sources — treated as first-class.
|
|
4
|
+
*/
|
|
5
|
+
import { type LlmExtract } from "../llm.js";
|
|
6
|
+
import { type ImportResult, type ParsedExport } from "./extract.js";
|
|
7
|
+
export declare function parseClaudeExport(dir: string): ParsedExport;
|
|
8
|
+
export declare function extractClaudeEvents(parsed: ParsedExport, extract?: LlmExtract, model?: string): Promise<ImportResult>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude data-export importer. Phase 0 finding: memories.json and projects
|
|
3
|
+
* are the highest-signal-per-byte sources — treated as first-class.
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, existsSync, readdirSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { anthropicExtract, DEFAULT_EXTRACT_MODEL } from "../llm.js";
|
|
8
|
+
import { extractEvents } from "./extract.js";
|
|
9
|
+
export function parseClaudeExport(dir) {
|
|
10
|
+
const convPath = join(dir, "conversations.json");
|
|
11
|
+
if (!existsSync(convPath))
|
|
12
|
+
throw new Error(`No conversations.json in ${dir}`);
|
|
13
|
+
const raw = JSON.parse(readFileSync(convPath, "utf-8"));
|
|
14
|
+
const conversations = raw.map((c) => ({
|
|
15
|
+
uuid: String(c.uuid ?? ""),
|
|
16
|
+
name: String(c.name ?? ""),
|
|
17
|
+
summary: String(c.summary ?? ""),
|
|
18
|
+
created_at: String(c.created_at ?? new Date().toISOString()),
|
|
19
|
+
userMessages: (c.chat_messages ?? [])
|
|
20
|
+
.filter((m) => m.sender === "human")
|
|
21
|
+
.map((m) => (m.text ? String(m.text) : textFromContent(m.content)))
|
|
22
|
+
.filter((t) => t.trim()),
|
|
23
|
+
}));
|
|
24
|
+
let memoryText = "";
|
|
25
|
+
const memPath = join(dir, "memories.json");
|
|
26
|
+
if (existsSync(memPath)) {
|
|
27
|
+
const memories = JSON.parse(readFileSync(memPath, "utf-8"));
|
|
28
|
+
memoryText = memories.map((m) => String(m.conversations_memory ?? "")).join("\n");
|
|
29
|
+
}
|
|
30
|
+
const projects = [];
|
|
31
|
+
const projDir = join(dir, "projects");
|
|
32
|
+
if (existsSync(projDir)) {
|
|
33
|
+
for (const f of readdirSync(projDir).filter((f) => f.endsWith(".json"))) {
|
|
34
|
+
const p = JSON.parse(readFileSync(join(projDir, f), "utf-8"));
|
|
35
|
+
if (p.is_starter_project)
|
|
36
|
+
continue;
|
|
37
|
+
projects.push({ name: String(p.name ?? ""), description: String(p.description ?? "") });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { conversations, memoryText, projects };
|
|
41
|
+
}
|
|
42
|
+
function textFromContent(content) {
|
|
43
|
+
if (typeof content === "string")
|
|
44
|
+
return content;
|
|
45
|
+
if (Array.isArray(content)) {
|
|
46
|
+
return content.map((c) => (c && typeof c === "object" ? String(c.text ?? "") : "")).join(" ");
|
|
47
|
+
}
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
export async function extractClaudeEvents(parsed, extract = anthropicExtract, model = DEFAULT_EXTRACT_MODEL) {
|
|
51
|
+
return extractEvents(parsed, { source: "import:claude", importer: "claude", file: "conversations.json" }, extract, model);
|
|
52
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared extraction pipeline for conversation-export importers.
|
|
3
|
+
* Parsers produce a ParsedExport; this turns it into provenance-linked events.
|
|
4
|
+
*/
|
|
5
|
+
import { type PersnallyEvent } from "../events.js";
|
|
6
|
+
import { type LlmExtract } from "../llm.js";
|
|
7
|
+
export interface ParsedConversation {
|
|
8
|
+
uuid: string;
|
|
9
|
+
name: string;
|
|
10
|
+
summary: string;
|
|
11
|
+
created_at: string;
|
|
12
|
+
userMessages: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface ParsedExport {
|
|
15
|
+
conversations: ParsedConversation[];
|
|
16
|
+
memoryText: string;
|
|
17
|
+
projects: {
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
}[];
|
|
21
|
+
}
|
|
22
|
+
export interface ImportResult {
|
|
23
|
+
events: PersnallyEvent[];
|
|
24
|
+
batch: string;
|
|
25
|
+
conversationsProcessed: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function extractEvents(parsed: ParsedExport, opts: {
|
|
28
|
+
source: string;
|
|
29
|
+
importer: string;
|
|
30
|
+
file: string;
|
|
31
|
+
}, extract?: LlmExtract, model?: string): Promise<ImportResult>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared extraction pipeline for conversation-export importers.
|
|
3
|
+
* Parsers produce a ParsedExport; this turns it into provenance-linked events.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { newEvent, uuidv7, PAYLOAD_SCHEMAS } from "../events.js";
|
|
7
|
+
import { anthropicExtract, DEFAULT_EXTRACT_MODEL } from "../llm.js";
|
|
8
|
+
const MAX_CONVO_CHARS = 30_000;
|
|
9
|
+
const topicsExtraction = z.object({ topics: z.array(PAYLOAD_SCHEMAS["signal.topic"]) });
|
|
10
|
+
const assertionsExtraction = z.object({ assertions: z.array(PAYLOAD_SCHEMAS["signal.assertion"]) });
|
|
11
|
+
export async function extractEvents(parsed, opts, extract = anthropicExtract, model = DEFAULT_EXTRACT_MODEL) {
|
|
12
|
+
const batch = uuidv7();
|
|
13
|
+
const events = [];
|
|
14
|
+
for (const convo of parsed.conversations) {
|
|
15
|
+
if (!convo.userMessages.length)
|
|
16
|
+
continue;
|
|
17
|
+
const text = convo.userMessages.join("\n").slice(0, MAX_CONVO_CHARS);
|
|
18
|
+
const result = await extract({
|
|
19
|
+
model,
|
|
20
|
+
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.",
|
|
21
|
+
schema: topicsExtraction,
|
|
22
|
+
content: `Conversation title: ${convo.name}\n\nUser messages:\n${text}`,
|
|
23
|
+
});
|
|
24
|
+
const { topics } = topicsExtraction.parse(result);
|
|
25
|
+
for (const t of topics) {
|
|
26
|
+
events.push(newEvent("signal.topic", opts.source, t, { kind: "import", batch, file: opts.file, conversation_uuid: convo.uuid }, new Date(convo.created_at).toISOString()));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if (parsed.memoryText.trim() || parsed.projects.length) {
|
|
30
|
+
const context = [
|
|
31
|
+
parsed.memoryText.trim() && `Assistant's accumulated memory of the user:\n${parsed.memoryText}`,
|
|
32
|
+
parsed.projects.length && `User-created projects:\n${parsed.projects.map((p) => `- ${p.name}: ${p.description}`).join("\n")}`,
|
|
33
|
+
].filter(Boolean).join("\n\n");
|
|
34
|
+
const result = await extract({
|
|
35
|
+
model,
|
|
36
|
+
instruction: "Extract structured assertions about this person: facts, preferences, behaviors, skills, and context. Confidence reflects how directly the source supports the claim.",
|
|
37
|
+
schema: assertionsExtraction,
|
|
38
|
+
content: context,
|
|
39
|
+
});
|
|
40
|
+
const { assertions } = assertionsExtraction.parse(result);
|
|
41
|
+
for (const a of assertions) {
|
|
42
|
+
events.push(newEvent("signal.assertion", opts.source, a, { kind: "import", batch, file: opts.file }));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const span = parsed.conversations.map((c) => c.created_at).sort();
|
|
46
|
+
events.push(newEvent("system.import", "system", {
|
|
47
|
+
importer: opts.importer,
|
|
48
|
+
batch,
|
|
49
|
+
events: events.length,
|
|
50
|
+
...(span.length ? { source_span: [span[0], span[span.length - 1]] } : {}),
|
|
51
|
+
}, { kind: "import", batch, file: opts.file }));
|
|
52
|
+
return { events, batch, conversationsProcessed: parsed.conversations.length };
|
|
53
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git history importer — fully deterministic, no LLM, works offline.
|
|
3
|
+
* Repos become project topics; manifest dependencies become skill signals.
|
|
4
|
+
* Carries forward the v1 skill_analyzer's framework-detection approach.
|
|
5
|
+
*/
|
|
6
|
+
import { type PersnallyEvent } from "../events.js";
|
|
7
|
+
export interface RepoSummary {
|
|
8
|
+
repo: string;
|
|
9
|
+
path: string;
|
|
10
|
+
commits: number;
|
|
11
|
+
firstCommit: string;
|
|
12
|
+
lastCommit: string;
|
|
13
|
+
frameworks: string[];
|
|
14
|
+
}
|
|
15
|
+
/** Pure: manifest filename → content → detected framework names. */
|
|
16
|
+
export declare function detectFrameworks(manifests: Record<string, string>): string[];
|
|
17
|
+
export declare function summarizeRepo(repoPath: string, authorEmail?: string): RepoSummary | null;
|
|
18
|
+
/** A path is either a repo or a directory of repos — resolve to repo summaries. */
|
|
19
|
+
export declare function scanRepos(path: string, authorEmail?: string): RepoSummary[];
|
|
20
|
+
export declare function gitEvents(summaries: RepoSummary[]): {
|
|
21
|
+
events: PersnallyEvent[];
|
|
22
|
+
batch: string;
|
|
23
|
+
};
|