ei-tui 0.1.25 → 0.3.1
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/README.md +42 -0
- package/package.json +2 -1
- package/src/README.md +4 -11
- package/src/cli/README.md +87 -7
- package/src/cli/commands/facts.ts +2 -2
- package/src/cli/commands/people.ts +2 -2
- package/src/cli/commands/quotes.ts +2 -2
- package/src/cli/commands/topics.ts +2 -2
- package/src/cli/mcp.ts +94 -0
- package/src/cli/retrieval.ts +67 -31
- package/src/cli.ts +64 -23
- package/src/core/AGENTS.md +1 -1
- package/src/core/constants/built-in-facts.ts +49 -0
- package/src/core/constants/index.ts +1 -0
- package/src/core/context-utils.ts +0 -1
- package/src/core/embedding-service.ts +8 -0
- package/src/core/handlers/dedup.ts +11 -23
- package/src/core/handlers/heartbeat.ts +2 -3
- package/src/core/handlers/human-extraction.ts +96 -30
- package/src/core/handlers/human-matching.ts +328 -248
- package/src/core/handlers/index.ts +8 -6
- package/src/core/handlers/persona-generation.ts +8 -8
- package/src/core/handlers/rewrite.ts +4 -51
- package/src/core/handlers/utils.ts +23 -1
- package/src/core/heartbeat-manager.ts +2 -4
- package/src/core/human-data-manager.ts +38 -36
- package/src/core/message-manager.ts +10 -10
- package/src/core/orchestrators/ceremony.ts +49 -44
- package/src/core/orchestrators/dedup-phase.ts +2 -4
- package/src/core/orchestrators/human-extraction.ts +351 -207
- package/src/core/orchestrators/index.ts +6 -4
- package/src/core/orchestrators/persona-generation.ts +3 -3
- package/src/core/processor.ts +167 -20
- package/src/core/prompt-context-builder.ts +4 -6
- package/src/core/state/human.ts +1 -26
- package/src/core/state/personas.ts +2 -2
- package/src/core/state-manager.ts +107 -14
- package/src/core/tools/builtin/read-memory.ts +13 -18
- package/src/core/types/data-items.ts +3 -4
- package/src/core/types/entities.ts +7 -4
- package/src/core/types/enums.ts +6 -9
- package/src/core/types/llm.ts +2 -2
- package/src/core/utils/crossFind.ts +2 -5
- package/src/core/utils/event-windows.ts +31 -0
- package/src/integrations/claude-code/importer.ts +14 -5
- package/src/integrations/claude-code/types.ts +3 -0
- package/src/integrations/cursor/importer.ts +282 -0
- package/src/integrations/cursor/index.ts +10 -0
- package/src/integrations/cursor/reader.ts +209 -0
- package/src/integrations/cursor/types.ts +140 -0
- package/src/integrations/opencode/importer.ts +14 -4
- package/src/prompts/AGENTS.md +73 -1
- package/src/prompts/ceremony/dedup.ts +0 -33
- package/src/prompts/ceremony/rewrite.ts +6 -41
- package/src/prompts/ceremony/types.ts +4 -4
- package/src/prompts/generation/descriptions.ts +2 -2
- package/src/prompts/generation/types.ts +2 -2
- package/src/prompts/heartbeat/types.ts +2 -2
- package/src/prompts/human/event-scan.ts +122 -0
- package/src/prompts/human/fact-find.ts +106 -0
- package/src/prompts/human/fact-scan.ts +0 -2
- package/src/prompts/human/index.ts +17 -10
- package/src/prompts/human/person-match.ts +65 -0
- package/src/prompts/human/person-scan.ts +52 -59
- package/src/prompts/human/person-update.ts +241 -0
- package/src/prompts/human/topic-match.ts +65 -0
- package/src/prompts/human/topic-scan.ts +51 -71
- package/src/prompts/human/topic-update.ts +295 -0
- package/src/prompts/human/types.ts +63 -40
- package/src/prompts/index.ts +4 -8
- package/src/prompts/persona/topics-update.ts +2 -2
- package/src/prompts/persona/traits.ts +2 -2
- package/src/prompts/persona/types.ts +3 -3
- package/src/prompts/response/index.ts +1 -1
- package/src/prompts/response/sections.ts +9 -12
- package/src/prompts/response/types.ts +2 -3
- package/src/storage/embeddings.ts +1 -1
- package/src/storage/index.ts +1 -0
- package/src/storage/indexed.ts +174 -0
- package/src/storage/merge.ts +67 -2
- package/tui/src/commands/me.tsx +5 -14
- package/tui/src/commands/settings.tsx +15 -0
- package/tui/src/context/ei.tsx +5 -14
- package/tui/src/util/yaml-serializers.ts +76 -33
- package/src/cli/commands/traits.ts +0 -25
- package/src/prompts/human/item-match.ts +0 -74
- package/src/prompts/human/item-update.ts +0 -364
- package/src/prompts/human/trait-scan.ts +0 -115
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
* Source of truth: CONTRACTS.md
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { ValidationLevel } from "./enums.js";
|
|
7
6
|
|
|
8
7
|
export interface DataItemBase {
|
|
9
8
|
id: string;
|
|
@@ -11,6 +10,7 @@ export interface DataItemBase {
|
|
|
11
10
|
description: string;
|
|
12
11
|
sentiment: number;
|
|
13
12
|
last_updated: string;
|
|
13
|
+
last_mentioned?: string; // Set by extraction only, never ceremony. Used for --recent sorting.
|
|
14
14
|
learned_by?: string; // Persona ID that originally learned this item (stable UUID)
|
|
15
15
|
last_changed_by?: string; // Persona ID that most recently updated this item (stable UUID)
|
|
16
16
|
persona_groups?: string[];
|
|
@@ -18,11 +18,10 @@ export interface DataItemBase {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export interface Fact extends DataItemBase {
|
|
21
|
-
validated: ValidationLevel;
|
|
22
21
|
validated_date: string;
|
|
23
22
|
}
|
|
24
23
|
|
|
25
|
-
export interface
|
|
24
|
+
export interface PersonaTrait extends DataItemBase {
|
|
26
25
|
strength?: number;
|
|
27
26
|
}
|
|
28
27
|
|
|
@@ -77,4 +76,4 @@ export interface Quote {
|
|
|
77
76
|
|
|
78
77
|
export type DataItemType = "fact" | "trait" | "topic" | "person";
|
|
79
78
|
|
|
80
|
-
export type DataItem = Fact |
|
|
79
|
+
export type DataItem = Fact | PersonaTrait | Topic | Person;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Source of truth: CONTRACTS.md
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { Fact,
|
|
6
|
+
import type { Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic } from "./data-items.js";
|
|
7
7
|
import type { ProviderType } from "./enums.js";
|
|
8
8
|
|
|
9
9
|
export interface SyncCredentials {
|
|
@@ -14,6 +14,8 @@ export interface SyncCredentials {
|
|
|
14
14
|
export interface OpenCodeSettings {
|
|
15
15
|
integration?: boolean;
|
|
16
16
|
polling_interval_ms?: number; // Default: 1800000 (30 min)
|
|
17
|
+
extraction_model?: string; // "Provider:model" for extraction. Unset = uses default_model.
|
|
18
|
+
extraction_token_limit?: number; // Token budget for extraction chunking. Unset = resolved from model.
|
|
17
19
|
last_sync?: string; // ISO timestamp
|
|
18
20
|
extraction_point?: string; // ISO timestamp - cursor for single-session archive scan
|
|
19
21
|
processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
|
|
@@ -25,6 +27,7 @@ export interface CeremonyConfig {
|
|
|
25
27
|
decay_rate?: number; // Default: 0.1
|
|
26
28
|
explore_threshold?: number; // Default: 3
|
|
27
29
|
dedup_threshold?: number; // Cosine similarity threshold for dedup candidates. Default: 0.85
|
|
30
|
+
event_window_hours?: number; // Gap threshold for conversation window detection. Default: 8
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
export interface BackupConfig {
|
|
@@ -83,12 +86,12 @@ export interface HumanSettings {
|
|
|
83
86
|
ceremony?: CeremonyConfig;
|
|
84
87
|
backup?: BackupConfig;
|
|
85
88
|
claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
|
|
89
|
+
cursor?: import("../../integrations/cursor/types.js").CursorSettings;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
export interface HumanEntity {
|
|
89
93
|
entity: "human";
|
|
90
94
|
facts: Fact[];
|
|
91
|
-
traits: Trait[];
|
|
92
95
|
topics: Topic[];
|
|
93
96
|
people: Person[];
|
|
94
97
|
quotes: Quote[];
|
|
@@ -107,7 +110,7 @@ export interface PersonaEntity {
|
|
|
107
110
|
model?: string;
|
|
108
111
|
group_primary?: string | null;
|
|
109
112
|
groups_visible?: string[];
|
|
110
|
-
traits:
|
|
113
|
+
traits: PersonaTrait[];
|
|
111
114
|
topics: PersonaTopic[];
|
|
112
115
|
is_paused: boolean;
|
|
113
116
|
pause_until?: string;
|
|
@@ -129,7 +132,7 @@ export interface PersonaCreationInput {
|
|
|
129
132
|
aliases?: string[];
|
|
130
133
|
long_description?: string;
|
|
131
134
|
short_description?: string;
|
|
132
|
-
traits?: Partial<
|
|
135
|
+
traits?: Partial<PersonaTrait>[];
|
|
133
136
|
topics?: Partial<PersonaTopic>[];
|
|
134
137
|
model?: string;
|
|
135
138
|
group_primary?: string;
|
package/src/core/types/enums.ts
CHANGED
|
@@ -9,11 +9,6 @@ export enum ContextStatus {
|
|
|
9
9
|
Never = "never",
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export enum ValidationLevel {
|
|
13
|
-
None = "none", // Fresh data, never acknowledged
|
|
14
|
-
Ei = "ei", // Ei mentioned it to user (don't mention again)
|
|
15
|
-
Human = "human", // User explicitly confirmed (locked)
|
|
16
|
-
}
|
|
17
12
|
export enum LLMRequestType {
|
|
18
13
|
Response = "response",
|
|
19
14
|
JSON = "json",
|
|
@@ -30,12 +25,13 @@ export enum LLMNextStep {
|
|
|
30
25
|
HandlePersonaResponse = "handlePersonaResponse",
|
|
31
26
|
HandlePersonaGeneration = "handlePersonaGeneration",
|
|
32
27
|
HandlePersonaDescriptions = "handlePersonaDescriptions",
|
|
33
|
-
|
|
34
|
-
HandleHumanTraitScan = "handleHumanTraitScan",
|
|
28
|
+
HandleFactFind = "handleFactFind",
|
|
35
29
|
HandleHumanTopicScan = "handleHumanTopicScan",
|
|
36
30
|
HandleHumanPersonScan = "handleHumanPersonScan",
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
HandleTopicMatch = "handleTopicMatch",
|
|
32
|
+
HandleTopicUpdate = "handleTopicUpdate",
|
|
33
|
+
HandlePersonMatch = "handlePersonMatch",
|
|
34
|
+
HandlePersonUpdate = "handlePersonUpdate",
|
|
39
35
|
HandlePersonaTraitExtraction = "handlePersonaTraitExtraction",
|
|
40
36
|
HandlePersonaTopicScan = "handlePersonaTopicScan",
|
|
41
37
|
HandlePersonaTopicMatch = "handlePersonaTopicMatch",
|
|
@@ -54,6 +50,7 @@ export enum LLMNextStep {
|
|
|
54
50
|
HandleRewriteScan = "handleRewriteScan",
|
|
55
51
|
HandleRewriteRewrite = "handleRewriteRewrite",
|
|
56
52
|
HandleDedupCurate = "handleDedupCurate",
|
|
53
|
+
HandleEventScan = "handleEventScan",
|
|
57
54
|
}
|
|
58
55
|
|
|
59
56
|
export enum ProviderType {
|
package/src/core/types/llm.ts
CHANGED
|
@@ -18,9 +18,9 @@ export interface Message {
|
|
|
18
18
|
// Extraction completion flags (omit when false to save space)
|
|
19
19
|
// Single-letter names minimize storage overhead for large message histories
|
|
20
20
|
f?: boolean; // Fact extraction completed
|
|
21
|
-
|
|
21
|
+
t?: boolean; // Topic extraction completed
|
|
22
22
|
p?: boolean; // Person extraction completed
|
|
23
|
-
|
|
23
|
+
e?: boolean; // Event (epic) extraction completed
|
|
24
24
|
// Image generation fields (web-only, ephemeral)
|
|
25
25
|
_synthesis?: boolean; // True if message was created by multi-message synthesis
|
|
26
26
|
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import type { HumanEntity, PersonaEntity, Fact,
|
|
1
|
+
import type { HumanEntity, PersonaEntity, Fact, PersonaTrait, Topic, Person, Quote, PersonaTopic } from "../types.ts";
|
|
2
2
|
export type CrossFindResult =
|
|
3
3
|
| { type: "fact" } & Fact
|
|
4
|
-
| { type: "trait" } & Trait
|
|
5
4
|
| { type: "topic" } & Topic
|
|
6
5
|
| { type: "person" } & Person
|
|
7
6
|
| { type: "quote" } & Quote
|
|
8
7
|
| { type: "persona" } & PersonaEntity
|
|
9
8
|
| { type: "personaTopic"; personaId: string } & PersonaTopic
|
|
10
|
-
| { type: "personaTrait"; personaId: string } &
|
|
9
|
+
| { type: "personaTrait"; personaId: string } & PersonaTrait;
|
|
11
10
|
|
|
12
11
|
export function crossFind(
|
|
13
12
|
id: string,
|
|
@@ -18,8 +17,6 @@ export function crossFind(
|
|
|
18
17
|
const fact = human.facts.find(f => f.id === id);
|
|
19
18
|
if (fact) return { type: "fact", ...fact };
|
|
20
19
|
|
|
21
|
-
const trait = human.traits.find(t => t.id === id);
|
|
22
|
-
if (trait) return { type: "trait", ...trait };
|
|
23
20
|
|
|
24
21
|
const person = human.people.find(p => p.id === id);
|
|
25
22
|
if (person) return { type: "person", ...person };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Message } from "../types.js";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_EVENT_WINDOW_HOURS = 8;
|
|
4
|
+
|
|
5
|
+
export function buildEventWindows(
|
|
6
|
+
messages: Message[],
|
|
7
|
+
gapHours: number = DEFAULT_EVENT_WINDOW_HOURS
|
|
8
|
+
): Message[][] {
|
|
9
|
+
if (messages.length === 0) return [];
|
|
10
|
+
|
|
11
|
+
const gapMs = gapHours * 60 * 60 * 1000;
|
|
12
|
+
const windows: Message[][] = [];
|
|
13
|
+
let currentWindow: Message[] = [messages[0]];
|
|
14
|
+
|
|
15
|
+
for (let i = 1; i < messages.length; i++) {
|
|
16
|
+
const prev = new Date(messages[i - 1].timestamp).getTime();
|
|
17
|
+
const curr = new Date(messages[i].timestamp).getTime();
|
|
18
|
+
|
|
19
|
+
if (curr - prev >= gapMs) {
|
|
20
|
+
windows.push(currentWindow);
|
|
21
|
+
currentWindow = [];
|
|
22
|
+
}
|
|
23
|
+
currentWindow.push(messages[i]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (currentWindow.length > 0) {
|
|
27
|
+
windows.push(currentWindow);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return windows;
|
|
31
|
+
}
|
|
@@ -54,9 +54,9 @@ function convertToPreMarkedEiMessage(msg: ClaudeCodeMessage): Message {
|
|
|
54
54
|
return {
|
|
55
55
|
...convertToEiMessage(msg),
|
|
56
56
|
f: true,
|
|
57
|
-
|
|
57
|
+
t: true,
|
|
58
58
|
p: true,
|
|
59
|
-
|
|
59
|
+
e: true,
|
|
60
60
|
};
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -130,7 +130,7 @@ function ensureSessionTopic(
|
|
|
130
130
|
exposure_current: 0.5,
|
|
131
131
|
exposure_desired: 0.3,
|
|
132
132
|
persona_groups: CLAUDE_CODE_TOPIC_GROUPS,
|
|
133
|
-
learned_by: CLAUDE_CODE_PERSONA_NAME,
|
|
133
|
+
learned_by: stateManager.persona_getByName(CLAUDE_CODE_PERSONA_NAME)?.id ?? undefined,
|
|
134
134
|
last_updated: new Date().toISOString(),
|
|
135
135
|
};
|
|
136
136
|
|
|
@@ -158,6 +158,10 @@ function updateProcessedState(
|
|
|
158
158
|
...human.settings,
|
|
159
159
|
claudeCode: {
|
|
160
160
|
...human.settings?.claudeCode,
|
|
161
|
+
// extraction_point is a progress indicator for user visibility only —
|
|
162
|
+
// it does NOT gate imports. processed_sessions is the sole source of
|
|
163
|
+
// truth for which sessions have been seen and when.
|
|
164
|
+
extraction_point: session.lastMessageAt,
|
|
161
165
|
processed_sessions: processedSessions,
|
|
162
166
|
},
|
|
163
167
|
},
|
|
@@ -212,7 +216,8 @@ export async function importClaudeCodeSessions(
|
|
|
212
216
|
|
|
213
217
|
// ─── Step 2: Find next unprocessed session ────────────────────────────
|
|
214
218
|
const human = stateManager.getHuman();
|
|
215
|
-
const
|
|
219
|
+
const settings = human.settings?.claudeCode;
|
|
220
|
+
const processedSessions = settings?.processed_sessions ?? {};
|
|
216
221
|
const now = Date.now();
|
|
217
222
|
|
|
218
223
|
let targetSession: ClaudeCodeSession | null = null;
|
|
@@ -305,7 +310,11 @@ export async function importClaudeCodeSessions(
|
|
|
305
310
|
messages_analyze: toAnalyze,
|
|
306
311
|
};
|
|
307
312
|
|
|
308
|
-
|
|
313
|
+
const ccSettings = stateManager.getHuman().settings?.claudeCode;
|
|
314
|
+
queueAllScans(context, stateManager, {
|
|
315
|
+
extraction_model: ccSettings?.extraction_model,
|
|
316
|
+
extraction_token_limit: ccSettings?.extraction_token_limit,
|
|
317
|
+
});
|
|
309
318
|
result.extractionScansQueued += 4;
|
|
310
319
|
}
|
|
311
320
|
|
|
@@ -158,6 +158,9 @@ export const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
|
|
|
158
158
|
export interface ClaudeCodeSettings {
|
|
159
159
|
integration?: boolean;
|
|
160
160
|
polling_interval_ms?: number; // Default: 1800000 (30 min)
|
|
161
|
+
extraction_model?: string; // "Provider:model" for extraction. Unset = uses default_model.
|
|
162
|
+
extraction_token_limit?: number; // Token budget for extraction chunking. Unset = resolved from model.
|
|
161
163
|
last_sync?: string; // ISO timestamp
|
|
164
|
+
extraction_point?: string; // ISO timestamp - floor cursor for processed-session skip
|
|
162
165
|
processed_sessions?: Record<string, string>; // sessionId → ISO timestamp of last import
|
|
163
166
|
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
+
import type { Ei_Interface, Topic, Message, ContextStatus, PersonaEntity } from "../../core/types.js";
|
|
3
|
+
import type { ICursorReader, CursorSession, CursorMessage } from "./types.js";
|
|
4
|
+
import {
|
|
5
|
+
CURSOR_PERSONA_NAME,
|
|
6
|
+
CURSOR_TOPIC_GROUPS,
|
|
7
|
+
MIN_SESSION_AGE_MS,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
import { CursorReader } from "./reader.js";
|
|
10
|
+
import {
|
|
11
|
+
queueAllScans,
|
|
12
|
+
type ExtractionContext,
|
|
13
|
+
} from "../../core/orchestrators/human-extraction.js";
|
|
14
|
+
|
|
15
|
+
export interface CursorImportResult {
|
|
16
|
+
sessionsProcessed: number;
|
|
17
|
+
topicsCreated: number;
|
|
18
|
+
topicsUpdated: number;
|
|
19
|
+
messagesImported: number;
|
|
20
|
+
personaCreated: boolean;
|
|
21
|
+
extractionScansQueued: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CursorImporterOptions {
|
|
25
|
+
stateManager: StateManager;
|
|
26
|
+
interface?: Ei_Interface;
|
|
27
|
+
reader?: ICursorReader;
|
|
28
|
+
signal?: AbortSignal;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TWELVE_HOURS_MS = 43_200_000;
|
|
32
|
+
const CURSOR_GROUP = "Cursor";
|
|
33
|
+
|
|
34
|
+
function convertToEiMessage(msg: CursorMessage): Message {
|
|
35
|
+
return {
|
|
36
|
+
id: msg.id,
|
|
37
|
+
role: msg.type === 1 ? "human" : "system",
|
|
38
|
+
verbal_response: msg.text,
|
|
39
|
+
timestamp: msg.timestamp,
|
|
40
|
+
read: true,
|
|
41
|
+
context_status: "default" as ContextStatus,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function convertToPreMarkedEiMessage(msg: CursorMessage): Message {
|
|
46
|
+
return {
|
|
47
|
+
...convertToEiMessage(msg),
|
|
48
|
+
f: true,
|
|
49
|
+
t: true,
|
|
50
|
+
p: true,
|
|
51
|
+
e: true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function ensureCursorPersona(
|
|
56
|
+
stateManager: StateManager,
|
|
57
|
+
eiInterface?: Ei_Interface
|
|
58
|
+
): PersonaEntity {
|
|
59
|
+
const existing = stateManager.persona_getByName(CURSOR_PERSONA_NAME);
|
|
60
|
+
if (existing) return existing;
|
|
61
|
+
|
|
62
|
+
const now = new Date().toISOString();
|
|
63
|
+
const persona: PersonaEntity = {
|
|
64
|
+
id: crypto.randomUUID(),
|
|
65
|
+
display_name: CURSOR_PERSONA_NAME,
|
|
66
|
+
entity: "system",
|
|
67
|
+
aliases: ["cursor", "cursor ide"],
|
|
68
|
+
short_description: "Cursor IDE — AI-powered coding environment",
|
|
69
|
+
long_description:
|
|
70
|
+
"Cursor is an AI-powered IDE that helps with coding tasks, debugging, architecture decisions, and more.",
|
|
71
|
+
group_primary: CURSOR_GROUP,
|
|
72
|
+
groups_visible: [CURSOR_GROUP],
|
|
73
|
+
traits: [],
|
|
74
|
+
topics: [],
|
|
75
|
+
is_paused: false,
|
|
76
|
+
is_archived: false,
|
|
77
|
+
is_static: false,
|
|
78
|
+
heartbeat_delay_ms: TWELVE_HOURS_MS,
|
|
79
|
+
last_heartbeat: now,
|
|
80
|
+
last_updated: now,
|
|
81
|
+
last_activity: now,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
stateManager.persona_add(persona);
|
|
85
|
+
eiInterface?.onPersonaAdded?.();
|
|
86
|
+
return persona;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function ensureSessionTopic(
|
|
90
|
+
session: CursorSession,
|
|
91
|
+
stateManager: StateManager
|
|
92
|
+
): "created" | "updated" | "unchanged" {
|
|
93
|
+
const human = stateManager.getHuman();
|
|
94
|
+
const existingTopic = human.topics.find((t) => t.id === session.id);
|
|
95
|
+
|
|
96
|
+
if (existingTopic) {
|
|
97
|
+
if (existingTopic.name !== session.name) {
|
|
98
|
+
const updatedTopic: Topic = {
|
|
99
|
+
...existingTopic,
|
|
100
|
+
name: session.name,
|
|
101
|
+
last_updated: new Date().toISOString(),
|
|
102
|
+
};
|
|
103
|
+
stateManager.human_topic_upsert(updatedTopic);
|
|
104
|
+
return "updated";
|
|
105
|
+
}
|
|
106
|
+
return "unchanged";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const newTopic: Topic = {
|
|
110
|
+
id: session.id,
|
|
111
|
+
name: session.name,
|
|
112
|
+
description: `Cursor session in ${session.workspacePath}`,
|
|
113
|
+
sentiment: 0,
|
|
114
|
+
exposure_current: 0.5,
|
|
115
|
+
exposure_desired: 0.3,
|
|
116
|
+
persona_groups: CURSOR_TOPIC_GROUPS,
|
|
117
|
+
learned_by: stateManager.persona_getByName(CURSOR_PERSONA_NAME)?.id ?? undefined,
|
|
118
|
+
last_updated: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
stateManager.human_topic_upsert(newTopic);
|
|
122
|
+
return "created";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function updateProcessedState(
|
|
126
|
+
stateManager: StateManager,
|
|
127
|
+
session: CursorSession
|
|
128
|
+
): void {
|
|
129
|
+
const human = stateManager.getHuman();
|
|
130
|
+
const lastMessageMs = new Date(session.lastMessageAt).getTime();
|
|
131
|
+
const extractionPoint = human.settings?.cursor?.extraction_point;
|
|
132
|
+
const currentPointMs = extractionPoint ? new Date(extractionPoint).getTime() : 0;
|
|
133
|
+
const newPointMs = Math.max(currentPointMs, lastMessageMs);
|
|
134
|
+
|
|
135
|
+
const processedSessions = {
|
|
136
|
+
...(human.settings?.cursor?.processed_sessions ?? {}),
|
|
137
|
+
[session.id]: new Date().toISOString(),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// extraction_point is a progress indicator for user visibility only —
|
|
141
|
+
// it does NOT gate imports. processed_sessions is the sole source of
|
|
142
|
+
// truth for which sessions have been seen and when.
|
|
143
|
+
stateManager.setHuman({
|
|
144
|
+
...human,
|
|
145
|
+
settings: {
|
|
146
|
+
...human.settings,
|
|
147
|
+
cursor: {
|
|
148
|
+
...human.settings?.cursor,
|
|
149
|
+
extraction_point: new Date(newPointMs).toISOString(),
|
|
150
|
+
processed_sessions: processedSessions,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export async function importCursorSessions(
|
|
157
|
+
options: CursorImporterOptions
|
|
158
|
+
): Promise<CursorImportResult> {
|
|
159
|
+
const { stateManager, interface: eiInterface, signal } = options;
|
|
160
|
+
const reader = options.reader ?? new CursorReader();
|
|
161
|
+
|
|
162
|
+
const result: CursorImportResult = {
|
|
163
|
+
sessionsProcessed: 0,
|
|
164
|
+
topicsCreated: 0,
|
|
165
|
+
topicsUpdated: 0,
|
|
166
|
+
messagesImported: 0,
|
|
167
|
+
personaCreated: false,
|
|
168
|
+
extractionScansQueued: 0,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const allSessions = await reader.getSessions();
|
|
172
|
+
|
|
173
|
+
for (const session of allSessions) {
|
|
174
|
+
const topicResult = ensureSessionTopic(session, stateManager);
|
|
175
|
+
if (topicResult === "created") result.topicsCreated++;
|
|
176
|
+
else if (topicResult === "updated") result.topicsUpdated++;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (signal?.aborted) return result;
|
|
180
|
+
if (result.topicsCreated > 0 || result.topicsUpdated > 0) {
|
|
181
|
+
eiInterface?.onHumanUpdated?.();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const human = stateManager.getHuman();
|
|
185
|
+
const processedSessions = human.settings?.cursor?.processed_sessions ?? {};
|
|
186
|
+
const now = Date.now();
|
|
187
|
+
|
|
188
|
+
let targetSession: CursorSession | null = null;
|
|
189
|
+
|
|
190
|
+
for (const session of allSessions) {
|
|
191
|
+
const sessionLastMs = new Date(session.lastMessageAt).getTime();
|
|
192
|
+
const ageMs = now - sessionLastMs;
|
|
193
|
+
|
|
194
|
+
if (ageMs < MIN_SESSION_AGE_MS) continue;
|
|
195
|
+
|
|
196
|
+
const lastImported = processedSessions[session.id];
|
|
197
|
+
if (lastImported && sessionLastMs <= new Date(lastImported).getTime()) continue;
|
|
198
|
+
|
|
199
|
+
targetSession = session;
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!targetSession) {
|
|
204
|
+
console.log("[Cursor] All sessions processed, nothing new to import");
|
|
205
|
+
return result;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (signal?.aborted) return result;
|
|
209
|
+
|
|
210
|
+
console.log(
|
|
211
|
+
`[Cursor] Processing session: "${targetSession.name}" ` +
|
|
212
|
+
`(last message: ${targetSession.lastMessageAt})`
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
const messages = targetSession.messages;
|
|
216
|
+
|
|
217
|
+
if (messages.length === 0) {
|
|
218
|
+
updateProcessedState(stateManager, targetSession);
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (signal?.aborted) return result;
|
|
223
|
+
|
|
224
|
+
const persona = ensureCursorPersona(stateManager, eiInterface);
|
|
225
|
+
result.personaCreated = !stateManager.persona_getByName(CURSOR_PERSONA_NAME);
|
|
226
|
+
|
|
227
|
+
if (!persona.is_archived) {
|
|
228
|
+
stateManager.persona_archive(persona.id);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
232
|
+
if (existingMsgs.length > 0) {
|
|
233
|
+
stateManager.messages_remove(persona.id, existingMsgs.map((m) => m.id));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
237
|
+
const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
|
|
238
|
+
const toAnalyze: Message[] = [];
|
|
239
|
+
|
|
240
|
+
for (const msg of messages) {
|
|
241
|
+
const msgMs = new Date(msg.timestamp).getTime();
|
|
242
|
+
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
243
|
+
const eiMsg = isOld ? convertToPreMarkedEiMessage(msg) : convertToEiMessage(msg);
|
|
244
|
+
stateManager.messages_append(persona.id, eiMsg);
|
|
245
|
+
result.messagesImported++;
|
|
246
|
+
if (!isOld) toAnalyze.push(eiMsg);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
stateManager.messages_sort(persona.id);
|
|
250
|
+
stateManager.persona_update(persona.id, {
|
|
251
|
+
last_activity: new Date().toISOString(),
|
|
252
|
+
});
|
|
253
|
+
eiInterface?.onMessageAdded?.(persona.id);
|
|
254
|
+
|
|
255
|
+
if (toAnalyze.length > 0 && !signal?.aborted) {
|
|
256
|
+
const allInState = stateManager.messages_get(persona.id);
|
|
257
|
+
const analyzeIds = new Set(toAnalyze.map((m) => m.id));
|
|
258
|
+
const analyzeStartIndex = allInState.findIndex((m) => analyzeIds.has(m.id));
|
|
259
|
+
const contextMsgs = analyzeStartIndex > 0 ? allInState.slice(0, analyzeStartIndex) : [];
|
|
260
|
+
|
|
261
|
+
const context: ExtractionContext = {
|
|
262
|
+
personaId: persona.id,
|
|
263
|
+
personaDisplayName: persona.display_name,
|
|
264
|
+
messages_context: contextMsgs,
|
|
265
|
+
messages_analyze: toAnalyze,
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
queueAllScans(context, stateManager, {});
|
|
269
|
+
result.extractionScansQueued += 4;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
result.sessionsProcessed = 1;
|
|
273
|
+
|
|
274
|
+
updateProcessedState(stateManager, targetSession);
|
|
275
|
+
|
|
276
|
+
console.log(
|
|
277
|
+
`[Cursor] Session complete: ${result.messagesImported} messages imported, ` +
|
|
278
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { CursorReader } from "./reader.js";
|
|
2
|
+
export { importCursorSessions } from "./importer.js";
|
|
3
|
+
export type { CursorImportResult, CursorImporterOptions } from "./importer.js";
|
|
4
|
+
export type {
|
|
5
|
+
ICursorReader,
|
|
6
|
+
CursorSession,
|
|
7
|
+
CursorMessage,
|
|
8
|
+
CursorSettings,
|
|
9
|
+
} from "./types.js";
|
|
10
|
+
export { CURSOR_PERSONA_NAME } from "./types.js";
|