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.
Files changed (88) hide show
  1. package/README.md +42 -0
  2. package/package.json +2 -1
  3. package/src/README.md +4 -11
  4. package/src/cli/README.md +87 -7
  5. package/src/cli/commands/facts.ts +2 -2
  6. package/src/cli/commands/people.ts +2 -2
  7. package/src/cli/commands/quotes.ts +2 -2
  8. package/src/cli/commands/topics.ts +2 -2
  9. package/src/cli/mcp.ts +94 -0
  10. package/src/cli/retrieval.ts +67 -31
  11. package/src/cli.ts +64 -23
  12. package/src/core/AGENTS.md +1 -1
  13. package/src/core/constants/built-in-facts.ts +49 -0
  14. package/src/core/constants/index.ts +1 -0
  15. package/src/core/context-utils.ts +0 -1
  16. package/src/core/embedding-service.ts +8 -0
  17. package/src/core/handlers/dedup.ts +11 -23
  18. package/src/core/handlers/heartbeat.ts +2 -3
  19. package/src/core/handlers/human-extraction.ts +96 -30
  20. package/src/core/handlers/human-matching.ts +328 -248
  21. package/src/core/handlers/index.ts +8 -6
  22. package/src/core/handlers/persona-generation.ts +8 -8
  23. package/src/core/handlers/rewrite.ts +4 -51
  24. package/src/core/handlers/utils.ts +23 -1
  25. package/src/core/heartbeat-manager.ts +2 -4
  26. package/src/core/human-data-manager.ts +38 -36
  27. package/src/core/message-manager.ts +10 -10
  28. package/src/core/orchestrators/ceremony.ts +49 -44
  29. package/src/core/orchestrators/dedup-phase.ts +2 -4
  30. package/src/core/orchestrators/human-extraction.ts +351 -207
  31. package/src/core/orchestrators/index.ts +6 -4
  32. package/src/core/orchestrators/persona-generation.ts +3 -3
  33. package/src/core/processor.ts +167 -20
  34. package/src/core/prompt-context-builder.ts +4 -6
  35. package/src/core/state/human.ts +1 -26
  36. package/src/core/state/personas.ts +2 -2
  37. package/src/core/state-manager.ts +107 -14
  38. package/src/core/tools/builtin/read-memory.ts +13 -18
  39. package/src/core/types/data-items.ts +3 -4
  40. package/src/core/types/entities.ts +7 -4
  41. package/src/core/types/enums.ts +6 -9
  42. package/src/core/types/llm.ts +2 -2
  43. package/src/core/utils/crossFind.ts +2 -5
  44. package/src/core/utils/event-windows.ts +31 -0
  45. package/src/integrations/claude-code/importer.ts +14 -5
  46. package/src/integrations/claude-code/types.ts +3 -0
  47. package/src/integrations/cursor/importer.ts +282 -0
  48. package/src/integrations/cursor/index.ts +10 -0
  49. package/src/integrations/cursor/reader.ts +209 -0
  50. package/src/integrations/cursor/types.ts +140 -0
  51. package/src/integrations/opencode/importer.ts +14 -4
  52. package/src/prompts/AGENTS.md +73 -1
  53. package/src/prompts/ceremony/dedup.ts +0 -33
  54. package/src/prompts/ceremony/rewrite.ts +6 -41
  55. package/src/prompts/ceremony/types.ts +4 -4
  56. package/src/prompts/generation/descriptions.ts +2 -2
  57. package/src/prompts/generation/types.ts +2 -2
  58. package/src/prompts/heartbeat/types.ts +2 -2
  59. package/src/prompts/human/event-scan.ts +122 -0
  60. package/src/prompts/human/fact-find.ts +106 -0
  61. package/src/prompts/human/fact-scan.ts +0 -2
  62. package/src/prompts/human/index.ts +17 -10
  63. package/src/prompts/human/person-match.ts +65 -0
  64. package/src/prompts/human/person-scan.ts +52 -59
  65. package/src/prompts/human/person-update.ts +241 -0
  66. package/src/prompts/human/topic-match.ts +65 -0
  67. package/src/prompts/human/topic-scan.ts +51 -71
  68. package/src/prompts/human/topic-update.ts +295 -0
  69. package/src/prompts/human/types.ts +63 -40
  70. package/src/prompts/index.ts +4 -8
  71. package/src/prompts/persona/topics-update.ts +2 -2
  72. package/src/prompts/persona/traits.ts +2 -2
  73. package/src/prompts/persona/types.ts +3 -3
  74. package/src/prompts/response/index.ts +1 -1
  75. package/src/prompts/response/sections.ts +9 -12
  76. package/src/prompts/response/types.ts +2 -3
  77. package/src/storage/embeddings.ts +1 -1
  78. package/src/storage/index.ts +1 -0
  79. package/src/storage/indexed.ts +174 -0
  80. package/src/storage/merge.ts +67 -2
  81. package/tui/src/commands/me.tsx +5 -14
  82. package/tui/src/commands/settings.tsx +15 -0
  83. package/tui/src/context/ei.tsx +5 -14
  84. package/tui/src/util/yaml-serializers.ts +76 -33
  85. package/src/cli/commands/traits.ts +0 -25
  86. package/src/prompts/human/item-match.ts +0 -74
  87. package/src/prompts/human/item-update.ts +0 -364
  88. 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 Trait extends DataItemBase {
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 | Trait | Topic | Person;
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, Trait, Topic, Person, Quote, PersonaTopic } from "./data-items.js";
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: Trait[];
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<Trait>[];
135
+ traits?: Partial<PersonaTrait>[];
133
136
  topics?: Partial<PersonaTopic>[];
134
137
  model?: string;
135
138
  group_primary?: string;
@@ -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
- HandleHumanFactScan = "handleHumanFactScan",
34
- HandleHumanTraitScan = "handleHumanTraitScan",
28
+ HandleFactFind = "handleFactFind",
35
29
  HandleHumanTopicScan = "handleHumanTopicScan",
36
30
  HandleHumanPersonScan = "handleHumanPersonScan",
37
- HandleHumanItemMatch = "handleHumanItemMatch",
38
- HandleHumanItemUpdate = "handleHumanItemUpdate",
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 {
@@ -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
- r?: boolean; // tRait extraction completed
21
+ t?: boolean; // Topic extraction completed
22
22
  p?: boolean; // Person extraction completed
23
- o?: boolean; // tOpic extraction completed
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, Trait, Topic, Person, Quote, PersonaTopic } from "../types.ts";
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 } & Trait;
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
- r: true,
57
+ t: true,
58
58
  p: true,
59
- o: true,
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 processedSessions = human.settings?.claudeCode?.processed_sessions ?? {};
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
- queueAllScans(context, stateManager);
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";