ei-tui 1.6.1 → 1.6.3

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.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Codex Integration Types
3
+ *
4
+ * Codex Desktop / CLI stores thread metadata in ~/.codex/state_*.sqlite and
5
+ * per-thread rollout JSONL files under ~/.codex/sessions/YYYY/MM/DD/.
6
+ * Ei imports only visible user/agent event messages and skips tool chatter,
7
+ * prompt scaffolding, token-count events, and system/developer payloads.
8
+ */
9
+
10
+ // ============================================================================
11
+ // Reader Interface
12
+ // ============================================================================
13
+
14
+ export interface CodexMessageWindow {
15
+ message: CodexMessage;
16
+ before: CodexMessage[];
17
+ after: CodexMessage[];
18
+ session: CodexSession;
19
+ }
20
+
21
+ export interface ICodexReader {
22
+ getSessions(): Promise<CodexSession[]>;
23
+ getMessageById(sessionId: string, messageId: string, before?: number, after?: number): Promise<CodexMessageWindow | null>;
24
+ isAvailable(): Promise<boolean>;
25
+ }
26
+
27
+ // ============================================================================
28
+ // Raw Storage Types
29
+ // ============================================================================
30
+
31
+ export interface CodexThreadRow {
32
+ id: string;
33
+ rollout_path?: string | null;
34
+ created_at?: number | null;
35
+ updated_at?: number | null;
36
+ created_at_ms?: number | null;
37
+ updated_at_ms?: number | null;
38
+ cwd?: string | null;
39
+ title?: string | null;
40
+ first_user_message?: string | null;
41
+ source?: string | null;
42
+ thread_source?: string | null;
43
+ agent_nickname?: string | null;
44
+ agent_role?: string | null;
45
+ agent_path?: string | null;
46
+ archived?: number | boolean | null;
47
+ }
48
+
49
+ export interface CodexRolloutRecord {
50
+ timestamp?: string;
51
+ type?: string;
52
+ payload?: Record<string, unknown>;
53
+ }
54
+
55
+ // ============================================================================
56
+ // Cleaned Session / Message Types
57
+ // ============================================================================
58
+
59
+ export interface CodexMessage {
60
+ /** Stable synthetic id derived from JSONL line number, e.g. "evt_42" */
61
+ id: string;
62
+ sessionId: string;
63
+ role: "user" | "assistant";
64
+ content: string;
65
+ timestamp: string;
66
+ }
67
+
68
+ export interface CodexSession {
69
+ id: string;
70
+ title: string;
71
+ cwd: string;
72
+ source?: string;
73
+ threadSource?: string;
74
+ agentNickname?: string;
75
+ agentRole?: string;
76
+ agentPath?: string;
77
+ rolloutPath: string;
78
+ firstMessageAt: string;
79
+ lastMessageAt: string;
80
+ messages: CodexMessage[];
81
+ }
82
+
83
+ // ============================================================================
84
+ // Constants
85
+ // ============================================================================
86
+
87
+ /** The single persona name for all Codex sessions */
88
+ export const CODEX_PERSONA_NAME = "Codex";
89
+
90
+ /** Topic groups assigned to Codex session topics */
91
+ export const CODEX_TOPIC_GROUPS = ["General", "Coding", "Codex"];
92
+
93
+ /**
94
+ * Minimum session age before import.
95
+ * Mirrors Claude Code / Cursor's 20-minute rule so active sessions can settle.
96
+ */
97
+ export { MIN_SESSION_AGE_MS } from "../constants.js";
98
+
99
+ // ============================================================================
100
+ // Human Settings Shape
101
+ // ============================================================================
102
+
103
+ /**
104
+ * Stored under human.settings.codex
105
+ *
106
+ * WARNING: ADDING A NEW FIELD HERE?
107
+ * If it is runtime-managed (not user-editable), also preserve it in
108
+ * settingsFromYAML() in tui/src/util/yaml-settings.ts or /settings will wipe it.
109
+ */
110
+ export interface CodexSettings {
111
+ integration?: boolean;
112
+ polling_interval_ms?: number;
113
+ extraction_model?: string;
114
+ last_sync?: string;
115
+ extraction_point?: string;
116
+ processed_sessions?: Record<string, string>;
117
+ }
@@ -0,0 +1,3 @@
1
+ export const MIN_SESSION_AGE_MS = 20 * 60 * 1_000;
2
+
3
+ export const TWELVE_HOURS_MS = 43_200_000;
@@ -1,15 +1,14 @@
1
1
  import type { StateManager } from "../../core/state-manager.js";
2
- import type { Ei_Interface, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
2
+ import type { Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
3
3
  import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
4
4
  import type { ICursorReader, CursorSession, CursorMessage } from "./types.js";
5
- import {
6
- CURSOR_PERSONA_NAME,
7
- MIN_SESSION_AGE_MS,
8
- } from "./types.js";
5
+ import { CURSOR_PERSONA_NAME } from "./types.js";
6
+ import { MIN_SESSION_AGE_MS, TWELVE_HOURS_MS } from "../constants.js";
9
7
  import { CursorReader } from "./reader.js";
10
8
  import { isProcessRunning } from "../process-check.js";
11
9
  import { getMachineId } from "../machine-id.js";
12
10
  import { qualifyCursorMessage } from "../../core/utils/message-id.js";
11
+ import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
13
12
  import {
14
13
  queueAllScans,
15
14
  type ExtractionContext,
@@ -33,29 +32,12 @@ export interface CursorImporterOptions {
33
32
  signal?: AbortSignal;
34
33
  }
35
34
 
36
- const TWELVE_HOURS_MS = 43_200_000;
37
35
  const CURSOR_GROUP = "Cursor";
38
36
 
39
- function convertToEiMessage(msg: CursorMessage, sessionId: string): Message {
40
- return {
41
- id: qualifyCursorMessage(getMachineId(), sessionId, msg.id),
42
- role: msg.type === 1 ? "human" : "system",
43
- content: msg.text,
44
- timestamp: msg.timestamp,
45
- read: true,
46
- context_status: "default" as ContextStatus,
47
- external: true,
48
- };
49
- }
37
+ const qualify = qualifyCursorMessage;
50
38
 
51
- function convertToPreMarkedEiMessage(msg: CursorMessage, sessionId: string): Message {
52
- return {
53
- ...convertToEiMessage(msg, sessionId),
54
- f: true,
55
- t: true,
56
- p: true,
57
- e: true,
58
- };
39
+ function normalizeCursorMessage(msg: CursorMessage) {
40
+ return { id: msg.id, role: (msg.type === 1 ? "user" : "assistant") as "user" | "assistant", content: msg.text, timestamp: msg.timestamp };
59
41
  }
60
42
 
61
43
  function ensureCursorPersona(
@@ -209,7 +191,8 @@ export async function importCursorSessions(
209
191
  for (const msg of messages) {
210
192
  const msgMs = new Date(msg.timestamp).getTime();
211
193
  const isOld = cutoffMs !== null && msgMs < cutoffMs;
212
- const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id) : convertToEiMessage(msg, targetSession.id);
194
+ const normalized = normalizeCursorMessage(msg);
195
+ const eiMsg = isOld ? convertToPreMarkedEiMessage(normalized, targetSession.id, qualify) : convertToEiMessage(normalized, targetSession.id, qualify);
213
196
  stateManager.messages_append(persona.id, eiMsg);
214
197
  result.messagesImported++;
215
198
  if (!isOld) toAnalyze.push(eiMsg);
@@ -116,7 +116,7 @@ export const CURSOR_TOPIC_GROUPS = ["General", "Coding", "Cursor"];
116
116
  * Minimum session age before we import it.
117
117
  * Mirrors ClaudeCode's 20-minute rule — gives the session time to "settle."
118
118
  */
119
- export const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
119
+ export { MIN_SESSION_AGE_MS } from "../constants.js";
120
120
 
121
121
  // ============================================================================
122
122
  // Human Settings Shape
@@ -11,19 +11,19 @@ export async function createOpenCodeReader(basePath?: string): Promise<IOpenCode
11
11
  if (existsSync(dbPath)) {
12
12
  try {
13
13
  const { SqliteReader } = await import("./sqlite-reader.js");
14
- console.log("[OpenCode] Using SQLite reader");
14
+ console.error("[OpenCode] Using SQLite reader");
15
15
  return new SqliteReader(dbPath);
16
16
  } catch {
17
- console.log("[OpenCode] SQLite not available, falling back to JSON reader");
17
+ console.error("[OpenCode] SQLite not available, falling back to JSON reader");
18
18
  }
19
19
  }
20
20
 
21
21
  if (existsSync(storagePath)) {
22
- console.log("[OpenCode] Using JSON reader (legacy)");
22
+ console.error("[OpenCode] Using JSON reader (legacy)");
23
23
  return new JsonReader(storagePath);
24
24
  }
25
25
 
26
- console.log("[OpenCode] No OpenCode data found");
26
+ console.error("[OpenCode] No OpenCode data found");
27
27
  return new JsonReader(storagePath);
28
28
  }
29
29
 
@@ -0,0 +1,235 @@
1
+ import type { StateManager } from "../../core/state-manager.js";
2
+ import type { Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
3
+ import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
4
+ import {
5
+ queueAllScans,
6
+ type ExtractionContext,
7
+ } from "../../core/orchestrators/human-extraction.js";
8
+ import {
9
+ queuePersonRewritePhase,
10
+ queueTopicRewritePhase,
11
+ } from "../../core/orchestrators/ceremony.js";
12
+ import { qualifyPiMessage } from "../../core/utils/message-id.js";
13
+ import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
14
+ import { getMachineId } from "../machine-id.js";
15
+ import { isProcessRunning } from "../process-check.js";
16
+ import { PiReader } from "./reader.js";
17
+ import {
18
+ PI_PERSONA_NAME,
19
+ type PiSession,
20
+ type IPiReader,
21
+ } from "./types.js";
22
+ import { MIN_SESSION_AGE_MS, TWELVE_HOURS_MS } from "../constants.js";
23
+
24
+ export interface PiImportResult {
25
+ sessionsProcessed: number;
26
+ messagesImported: number;
27
+ personaCreated: boolean;
28
+ extractionScansQueued: number;
29
+ }
30
+
31
+ export interface PiImporterOptions {
32
+ stateManager: StateManager;
33
+ interface?: Ei_Interface;
34
+ reader?: IPiReader;
35
+ signal?: AbortSignal;
36
+ }
37
+
38
+ const PI_GROUP = "Pi";
39
+
40
+ const qualify = qualifyPiMessage;
41
+
42
+ function ensurePiPersona(
43
+ stateManager: StateManager,
44
+ eiInterface?: Ei_Interface
45
+ ): PersonaEntity {
46
+ const existing = stateManager.persona_getByName(PI_PERSONA_NAME);
47
+ if (existing) return existing;
48
+
49
+ const now = new Date().toISOString();
50
+ const seedTraits: PersonaTrait[] = DEFAULT_SEED_TRAITS.map((t) => ({
51
+ id: crypto.randomUUID(),
52
+ name: t.name,
53
+ description: t.description,
54
+ sentiment: t.sentiment,
55
+ strength: t.strength,
56
+ last_updated: now,
57
+ }));
58
+
59
+ const persona: PersonaEntity = {
60
+ id: crypto.randomUUID(),
61
+ display_name: PI_PERSONA_NAME,
62
+ entity: "system",
63
+ aliases: ["pi", "pi coding agent", "omp", "oh-my-pi"],
64
+ short_description: "Pi - minimal terminal coding harness",
65
+ long_description:
66
+ "Pi is a minimal terminal coding harness. Covers both vanilla Pi (earendil-works/pi) and the oh-my-pi fork (omp), which share the same JSONL session format.",
67
+ group_primary: PI_GROUP,
68
+ groups_visible: [PI_GROUP],
69
+ traits: seedTraits,
70
+ topics: [],
71
+ is_paused: false,
72
+ is_archived: false,
73
+ is_static: false,
74
+ heartbeat_delay_ms: TWELVE_HOURS_MS,
75
+ last_heartbeat: now,
76
+ last_updated: now,
77
+ };
78
+
79
+ stateManager.persona_add(persona);
80
+ eiInterface?.onPersonaAdded?.();
81
+ return persona;
82
+ }
83
+
84
+ function updateProcessedState(stateManager: StateManager, session: PiSession): void {
85
+ const human = stateManager.getHuman();
86
+ const lastMessageMs = new Date(session.lastMessageAt).getTime();
87
+ const extractionPoint = human.settings?.pi?.extraction_point;
88
+ const currentPointMs = extractionPoint ? new Date(extractionPoint).getTime() : 0;
89
+ const newPointMs = Math.max(currentPointMs, lastMessageMs);
90
+
91
+ const processedSessions = {
92
+ ...(human.settings?.pi?.processed_sessions ?? {}),
93
+ [session.id]: new Date().toISOString(),
94
+ };
95
+
96
+ stateManager.setHuman({
97
+ ...human,
98
+ settings: {
99
+ ...human.settings,
100
+ pi: {
101
+ ...human.settings?.pi,
102
+ extraction_point: new Date(newPointMs).toISOString(),
103
+ processed_sessions: processedSessions,
104
+ },
105
+ },
106
+ });
107
+ }
108
+
109
+ async function isPiRunning(): Promise<boolean> {
110
+ return (
111
+ (await isProcessRunning("pi")) ||
112
+ (await isProcessRunning("omp"))
113
+ );
114
+ }
115
+
116
+ export async function importPiSessions(options: PiImporterOptions): Promise<PiImportResult> {
117
+ const { stateManager, interface: eiInterface, signal } = options;
118
+ const reader = options.reader ?? new PiReader();
119
+
120
+ const result: PiImportResult = {
121
+ sessionsProcessed: 0,
122
+ messagesImported: 0,
123
+ personaCreated: false,
124
+ extractionScansQueued: 0,
125
+ };
126
+
127
+ const allSessions = await reader.getSessions();
128
+ if (signal?.aborted) return result;
129
+
130
+ const human = stateManager.getHuman();
131
+ const processedSessions = human.settings?.pi?.processed_sessions ?? {};
132
+ const now = Date.now();
133
+ const toolRunning = await isPiRunning();
134
+
135
+ let targetSession: PiSession | null = null;
136
+
137
+ for (const session of allSessions) {
138
+ const sessionLastMs = new Date(session.lastMessageAt).getTime();
139
+ const ageMs = now - sessionLastMs;
140
+
141
+ if (ageMs < MIN_SESSION_AGE_MS && toolRunning) continue;
142
+
143
+ const lastImported = processedSessions[session.id];
144
+ if (lastImported && sessionLastMs <= new Date(lastImported).getTime()) continue;
145
+
146
+ targetSession = session;
147
+ break;
148
+ }
149
+
150
+ if (!targetSession) {
151
+ console.log("[Pi] All sessions processed, nothing new to import");
152
+ return result;
153
+ }
154
+
155
+ if (signal?.aborted) return result;
156
+
157
+ console.log(
158
+ `[Pi] Processing session: "${targetSession.title}" ` +
159
+ `(last message: ${targetSession.lastMessageAt})`
160
+ );
161
+
162
+ const messages = targetSession.messages;
163
+ if (messages.length === 0) {
164
+ updateProcessedState(stateManager, targetSession);
165
+ return result;
166
+ }
167
+
168
+ if (signal?.aborted) return result;
169
+
170
+ const personaExistedBefore = stateManager.persona_getByName(PI_PERSONA_NAME) !== null;
171
+ const persona = ensurePiPersona(stateManager, eiInterface);
172
+ result.personaCreated = !personaExistedBefore;
173
+
174
+ if (!personaExistedBefore) {
175
+ stateManager.persona_archive(persona.id);
176
+ } else {
177
+ const existingMsgs = stateManager.messages_get(persona.id);
178
+ const externalIds = existingMsgs.filter((m) => m.external === true).map((m) => m.id);
179
+ if (externalIds.length > 0) {
180
+ stateManager.messages_remove(persona.id, externalIds);
181
+ }
182
+ }
183
+
184
+ const cutoffIso = processedSessions[targetSession.id] ?? null;
185
+ const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
186
+ const toAnalyze: Message[] = [];
187
+
188
+ for (const msg of messages) {
189
+ const msgMs = new Date(msg.timestamp).getTime();
190
+ const isOld = cutoffMs !== null && msgMs < cutoffMs;
191
+ const eiMsg = isOld
192
+ ? convertToPreMarkedEiMessage(msg, targetSession.id, qualify)
193
+ : convertToEiMessage(msg, targetSession.id, qualify);
194
+
195
+ stateManager.messages_append(persona.id, eiMsg);
196
+ result.messagesImported++;
197
+ if (!isOld) toAnalyze.push(eiMsg);
198
+ }
199
+
200
+ stateManager.messages_sort(persona.id);
201
+ eiInterface?.onMessageAdded?.(persona.id);
202
+
203
+ if (toAnalyze.length > 0 && !signal?.aborted) {
204
+ const allInState = stateManager.messages_get(persona.id);
205
+ const analyzeIds = new Set(toAnalyze.map((m) => m.id));
206
+ const analyzeStartIndex = allInState.findIndex((m) => analyzeIds.has(m.id));
207
+ const contextMsgs = analyzeStartIndex > 0 ? allInState.slice(0, analyzeStartIndex) : [];
208
+
209
+ const context: ExtractionContext = {
210
+ personaId: persona.id,
211
+ channelDisplayName: persona.display_name,
212
+ messages_context: contextMsgs,
213
+ messages_analyze: toAnalyze,
214
+ sources: [`pi:${getMachineId()}:${targetSession.id}`],
215
+ };
216
+
217
+ queuePersonRewritePhase(stateManager);
218
+ queueTopicRewritePhase(stateManager);
219
+ queueAllScans(context, stateManager, {
220
+ extraction_model: human.settings?.pi?.extraction_model,
221
+ external_filter: "only",
222
+ });
223
+ result.extractionScansQueued += 4;
224
+ }
225
+
226
+ result.sessionsProcessed = 1;
227
+ updateProcessedState(stateManager, targetSession);
228
+
229
+ console.log(
230
+ `[Pi] Session complete: ${result.messagesImported} messages imported, ` +
231
+ `${result.extractionScansQueued} extraction scans queued`
232
+ );
233
+
234
+ return result;
235
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./types.js";
2
+ export * from "./reader.js";
3
+ export * from "./importer.js";