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.
- package/README.md +21 -6
- package/package.json +1 -1
- package/src/cli/README.md +11 -7
- package/src/cli/mcp.ts +3 -3
- package/src/cli/retrieval.ts +44 -0
- package/src/cli.ts +352 -14
- package/src/core/context-utils.ts +0 -1
- package/src/core/orchestrators/ceremony.ts +1 -1
- package/src/core/processor.ts +150 -0
- package/src/core/types/data-items.ts +1 -1
- package/src/core/types/entities.ts +2 -0
- package/src/core/types/llm.ts +1 -1
- package/src/core/utils/message-id.ts +31 -0
- package/src/integrations/claude-code/importer.ts +9 -30
- package/src/integrations/claude-code/types.ts +1 -1
- package/src/integrations/codex/importer.ts +237 -0
- package/src/integrations/codex/index.ts +11 -0
- package/src/integrations/codex/reader.ts +241 -0
- package/src/integrations/codex/types.ts +117 -0
- package/src/integrations/constants.ts +3 -0
- package/src/integrations/cursor/importer.ts +9 -26
- package/src/integrations/cursor/types.ts +1 -1
- package/src/integrations/opencode/reader-factory.ts +4 -4
- package/src/integrations/pi/importer.ts +235 -0
- package/src/integrations/pi/index.ts +3 -0
- package/src/integrations/pi/reader.ts +247 -0
- package/src/integrations/pi/types.ts +151 -0
- package/src/integrations/shared/message-converter.ts +41 -0
- package/tui/README.md +5 -3
- package/tui/src/util/yaml-settings.ts +56 -0
package/src/core/types/llm.ts
CHANGED
|
@@ -25,7 +25,7 @@ export interface Message {
|
|
|
25
25
|
_synthesis?: boolean; // True if message was created by multi-message synthesis
|
|
26
26
|
speaker_name?: string; // Display name of actual speaker; set on room messages for clean hydration
|
|
27
27
|
|
|
28
|
-
external?: boolean; // Set by integration importers (OpenCode, Cursor, Claude Code); invisible to LLM context
|
|
28
|
+
external?: boolean; // Set by integration importers (OpenCode, Cursor, Claude Code, Codex); invisible to LLM context
|
|
29
29
|
|
|
30
30
|
}
|
|
31
31
|
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* opencode:${machine}:${session}:${nativeId}
|
|
5
5
|
* claudecode:${machine}:${session}:${nativeId}
|
|
6
6
|
* cursor:${machine}:${session}:${nativeId}
|
|
7
|
+
* codex:${machine}:${session}:${nativeId}
|
|
7
8
|
* import:document:${slug}:${uuid}
|
|
8
9
|
* slack:${workspace}:${channel}:${ts}
|
|
9
10
|
*/
|
|
@@ -13,6 +14,8 @@ export type MessageIdIntegration =
|
|
|
13
14
|
| "opencode"
|
|
14
15
|
| "claudecode"
|
|
15
16
|
| "cursor"
|
|
17
|
+
| "codex"
|
|
18
|
+
| "pi"
|
|
16
19
|
| "import"
|
|
17
20
|
| "slack"
|
|
18
21
|
| "unknown"
|
|
@@ -67,6 +70,26 @@ export function parseMessageId(id: string): ParsedMessageId {
|
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
|
|
73
|
+
if (parts[0] === "codex" && parts.length >= 4) {
|
|
74
|
+
return {
|
|
75
|
+
integration: "codex",
|
|
76
|
+
machine: parts[1],
|
|
77
|
+
session: parts[2],
|
|
78
|
+
nativeId: parts.slice(3).join(":"),
|
|
79
|
+
raw: id,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (parts[0] === "pi" && parts.length >= 4) {
|
|
84
|
+
return {
|
|
85
|
+
integration: "pi",
|
|
86
|
+
machine: parts[1],
|
|
87
|
+
session: parts[2],
|
|
88
|
+
nativeId: parts.slice(3).join(":"),
|
|
89
|
+
raw: id,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
70
93
|
if (parts[0] === "import" && parts[1] === "document" && parts.length >= 4) {
|
|
71
94
|
return {
|
|
72
95
|
integration: "import",
|
|
@@ -109,6 +132,14 @@ export function qualifyCursorMessage(machine: string, sessionId: string, nativeI
|
|
|
109
132
|
return `cursor:${machine}:${sessionId}:${nativeId}`
|
|
110
133
|
}
|
|
111
134
|
|
|
135
|
+
export function qualifyCodexMessage(machine: string, sessionId: string, nativeId: string): string {
|
|
136
|
+
return `codex:${machine}:${sessionId}:${nativeId}`
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function qualifyPiMessage(machine: string, sessionId: string, nativeId: string): string {
|
|
140
|
+
return `pi:${machine}:${sessionId}:${nativeId}`
|
|
141
|
+
}
|
|
142
|
+
|
|
112
143
|
export function qualifyDocumentMessage(slug: string, uuid: string): string {
|
|
113
144
|
return `import:document:${slug}:${uuid}`
|
|
114
145
|
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
-
import type { Ei_Interface, Message,
|
|
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
|
-
import type { IClaudeCodeReader, ClaudeCodeSession
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
MIN_SESSION_AGE_MS,
|
|
8
|
-
} from "./types.js";
|
|
4
|
+
import type { IClaudeCodeReader, ClaudeCodeSession } from "./types.js";
|
|
5
|
+
import { CLAUDE_CODE_PERSONA_NAME } from "./types.js";
|
|
6
|
+
import { MIN_SESSION_AGE_MS } from "../constants.js";
|
|
9
7
|
import { ClaudeCodeReader } from "./reader.js";
|
|
10
8
|
import {
|
|
11
9
|
queueAllScans,
|
|
@@ -16,8 +14,10 @@ import {
|
|
|
16
14
|
queueTopicRewritePhase,
|
|
17
15
|
} from "../../core/orchestrators/ceremony.js";
|
|
18
16
|
import { isProcessRunning } from "../process-check.js";
|
|
19
|
-
import { getMachineId } from "../machine-id.js";
|
|
20
17
|
import { qualifyClaudeCodeMessage } from "../../core/utils/message-id.js";
|
|
18
|
+
import { getMachineId } from "../machine-id.js";
|
|
19
|
+
import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
|
|
20
|
+
import { TWELVE_HOURS_MS } from "../constants.js";
|
|
21
21
|
|
|
22
22
|
// =============================================================================
|
|
23
23
|
// Export Types
|
|
@@ -41,30 +41,9 @@ export interface ClaudeCodeImporterOptions {
|
|
|
41
41
|
// Utility Functions
|
|
42
42
|
// =============================================================================
|
|
43
43
|
|
|
44
|
-
const TWELVE_HOURS_MS = 43_200_000;
|
|
45
44
|
const CLAUDE_CODE_GROUP = "Claude Code";
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
return {
|
|
49
|
-
id: qualifyClaudeCodeMessage(getMachineId(), sessionId, msg.id),
|
|
50
|
-
role: msg.role === "user" ? "human" : "system",
|
|
51
|
-
content: msg.content,
|
|
52
|
-
timestamp: msg.timestamp,
|
|
53
|
-
read: true,
|
|
54
|
-
context_status: "default" as ContextStatus,
|
|
55
|
-
external: true,
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function convertToPreMarkedEiMessage(msg: ClaudeCodeMessage, sessionId: string): Message {
|
|
60
|
-
return {
|
|
61
|
-
...convertToEiMessage(msg, sessionId),
|
|
62
|
-
f: true,
|
|
63
|
-
t: true,
|
|
64
|
-
p: true,
|
|
65
|
-
e: true,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
46
|
+
const qualify = qualifyClaudeCodeMessage;
|
|
68
47
|
|
|
69
48
|
/**
|
|
70
49
|
* Ensure the single "Claude Code" persona exists.
|
|
@@ -249,7 +228,7 @@ export async function importClaudeCodeSessions(
|
|
|
249
228
|
for (const msg of messages) {
|
|
250
229
|
const msgMs = new Date(msg.timestamp).getTime();
|
|
251
230
|
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
252
|
-
const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id) : convertToEiMessage(msg, targetSession.id);
|
|
231
|
+
const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id, qualify) : convertToEiMessage(msg, targetSession.id, qualify);
|
|
253
232
|
stateManager.messages_append(persona.id, eiMsg);
|
|
254
233
|
result.messagesImported++;
|
|
255
234
|
if (!isOld) toAnalyze.push(eiMsg);
|
|
@@ -139,7 +139,7 @@ export const CLAUDE_CODE_TOPIC_GROUPS = ["General", "Coding", "Claude Code"];
|
|
|
139
139
|
* Minimum session age before we import it.
|
|
140
140
|
* Mirrors OpenCode's 20-minute rule — gives the session time to "settle."
|
|
141
141
|
*/
|
|
142
|
-
export
|
|
142
|
+
export { MIN_SESSION_AGE_MS } from "../constants.js";
|
|
143
143
|
|
|
144
144
|
// ============================================================================
|
|
145
145
|
// Human Settings Shape (mirrors OpenCodeSettings in core/types.ts)
|
|
@@ -0,0 +1,237 @@
|
|
|
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 { qualifyCodexMessage } 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 { CodexReader } from "./reader.js";
|
|
17
|
+
import {
|
|
18
|
+
CODEX_PERSONA_NAME,
|
|
19
|
+
type CodexSession,
|
|
20
|
+
type ICodexReader,
|
|
21
|
+
} from "./types.js";
|
|
22
|
+
import { MIN_SESSION_AGE_MS, TWELVE_HOURS_MS } from "../constants.js";
|
|
23
|
+
|
|
24
|
+
export interface CodexImportResult {
|
|
25
|
+
sessionsProcessed: number;
|
|
26
|
+
messagesImported: number;
|
|
27
|
+
personaCreated: boolean;
|
|
28
|
+
extractionScansQueued: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CodexImporterOptions {
|
|
32
|
+
stateManager: StateManager;
|
|
33
|
+
interface?: Ei_Interface;
|
|
34
|
+
reader?: ICodexReader;
|
|
35
|
+
signal?: AbortSignal;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const CODEX_GROUP = "Codex";
|
|
39
|
+
|
|
40
|
+
const qualify = qualifyCodexMessage;
|
|
41
|
+
|
|
42
|
+
function ensureCodexPersona(
|
|
43
|
+
stateManager: StateManager,
|
|
44
|
+
eiInterface?: Ei_Interface
|
|
45
|
+
): PersonaEntity {
|
|
46
|
+
const existing = stateManager.persona_getByName(CODEX_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: CODEX_PERSONA_NAME,
|
|
62
|
+
entity: "system",
|
|
63
|
+
aliases: ["codex", "codex cli", "codex desktop", "openai codex"],
|
|
64
|
+
short_description: "Codex - OpenAI coding agent environment",
|
|
65
|
+
long_description:
|
|
66
|
+
"Codex is OpenAI's coding agent environment for working with local codebases, terminal commands, tools, and implementation tasks.",
|
|
67
|
+
group_primary: CODEX_GROUP,
|
|
68
|
+
groups_visible: [CODEX_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(
|
|
85
|
+
stateManager: StateManager,
|
|
86
|
+
session: CodexSession
|
|
87
|
+
): void {
|
|
88
|
+
const human = stateManager.getHuman();
|
|
89
|
+
const lastMessageMs = new Date(session.lastMessageAt).getTime();
|
|
90
|
+
const extractionPoint = human.settings?.codex?.extraction_point;
|
|
91
|
+
const currentPointMs = extractionPoint ? new Date(extractionPoint).getTime() : 0;
|
|
92
|
+
const newPointMs = Math.max(currentPointMs, lastMessageMs);
|
|
93
|
+
|
|
94
|
+
const processedSessions = {
|
|
95
|
+
...(human.settings?.codex?.processed_sessions ?? {}),
|
|
96
|
+
[session.id]: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
stateManager.setHuman({
|
|
100
|
+
...human,
|
|
101
|
+
settings: {
|
|
102
|
+
...human.settings,
|
|
103
|
+
codex: {
|
|
104
|
+
...human.settings?.codex,
|
|
105
|
+
extraction_point: new Date(newPointMs).toISOString(),
|
|
106
|
+
processed_sessions: processedSessions,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function isCodexRunning(): Promise<boolean> {
|
|
113
|
+
return (await isProcessRunning("Codex")) || (await isProcessRunning("codex"));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function importCodexSessions(
|
|
117
|
+
options: CodexImporterOptions
|
|
118
|
+
): Promise<CodexImportResult> {
|
|
119
|
+
const { stateManager, interface: eiInterface, signal } = options;
|
|
120
|
+
const reader = options.reader ?? new CodexReader();
|
|
121
|
+
|
|
122
|
+
const result: CodexImportResult = {
|
|
123
|
+
sessionsProcessed: 0,
|
|
124
|
+
messagesImported: 0,
|
|
125
|
+
personaCreated: false,
|
|
126
|
+
extractionScansQueued: 0,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const allSessions = await reader.getSessions();
|
|
130
|
+
if (signal?.aborted) return result;
|
|
131
|
+
|
|
132
|
+
const human = stateManager.getHuman();
|
|
133
|
+
const processedSessions = human.settings?.codex?.processed_sessions ?? {};
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
const toolRunning = await isCodexRunning();
|
|
136
|
+
|
|
137
|
+
let targetSession: CodexSession | null = null;
|
|
138
|
+
|
|
139
|
+
for (const session of allSessions) {
|
|
140
|
+
const sessionLastMs = new Date(session.lastMessageAt).getTime();
|
|
141
|
+
const ageMs = now - sessionLastMs;
|
|
142
|
+
|
|
143
|
+
if (ageMs < MIN_SESSION_AGE_MS && toolRunning) continue;
|
|
144
|
+
|
|
145
|
+
const lastImported = processedSessions[session.id];
|
|
146
|
+
if (lastImported && sessionLastMs <= new Date(lastImported).getTime()) continue;
|
|
147
|
+
|
|
148
|
+
targetSession = session;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!targetSession) {
|
|
153
|
+
console.log("[Codex] All sessions processed, nothing new to import");
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (signal?.aborted) return result;
|
|
158
|
+
|
|
159
|
+
console.log(
|
|
160
|
+
`[Codex] Processing session: "${targetSession.title}" ` +
|
|
161
|
+
`(last message: ${targetSession.lastMessageAt})`
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
const messages = targetSession.messages;
|
|
165
|
+
if (messages.length === 0) {
|
|
166
|
+
updateProcessedState(stateManager, targetSession);
|
|
167
|
+
return result;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (signal?.aborted) return result;
|
|
171
|
+
|
|
172
|
+
const personaExistedBefore = stateManager.persona_getByName(CODEX_PERSONA_NAME) !== null;
|
|
173
|
+
const persona = ensureCodexPersona(stateManager, eiInterface);
|
|
174
|
+
result.personaCreated = !personaExistedBefore;
|
|
175
|
+
|
|
176
|
+
if (!personaExistedBefore) {
|
|
177
|
+
stateManager.persona_archive(persona.id);
|
|
178
|
+
} else {
|
|
179
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
180
|
+
const externalIds = existingMsgs.filter((m) => m.external === true).map((m) => m.id);
|
|
181
|
+
if (externalIds.length > 0) {
|
|
182
|
+
stateManager.messages_remove(persona.id, externalIds);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
187
|
+
const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
|
|
188
|
+
const toAnalyze: Message[] = [];
|
|
189
|
+
|
|
190
|
+
for (const msg of messages) {
|
|
191
|
+
const msgMs = new Date(msg.timestamp).getTime();
|
|
192
|
+
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
193
|
+
const eiMsg = isOld
|
|
194
|
+
? convertToPreMarkedEiMessage(msg, targetSession.id, qualify)
|
|
195
|
+
: convertToEiMessage(msg, targetSession.id, qualify);
|
|
196
|
+
|
|
197
|
+
stateManager.messages_append(persona.id, eiMsg);
|
|
198
|
+
result.messagesImported++;
|
|
199
|
+
if (!isOld) toAnalyze.push(eiMsg);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
stateManager.messages_sort(persona.id);
|
|
203
|
+
eiInterface?.onMessageAdded?.(persona.id);
|
|
204
|
+
|
|
205
|
+
if (toAnalyze.length > 0 && !signal?.aborted) {
|
|
206
|
+
const allInState = stateManager.messages_get(persona.id);
|
|
207
|
+
const analyzeIds = new Set(toAnalyze.map((m) => m.id));
|
|
208
|
+
const analyzeStartIndex = allInState.findIndex((m) => analyzeIds.has(m.id));
|
|
209
|
+
const contextMsgs = analyzeStartIndex > 0 ? allInState.slice(0, analyzeStartIndex) : [];
|
|
210
|
+
|
|
211
|
+
const context: ExtractionContext = {
|
|
212
|
+
personaId: persona.id,
|
|
213
|
+
channelDisplayName: persona.display_name,
|
|
214
|
+
messages_context: contextMsgs,
|
|
215
|
+
messages_analyze: toAnalyze,
|
|
216
|
+
sources: [`codex:${getMachineId()}:${targetSession.id}`],
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
queuePersonRewritePhase(stateManager);
|
|
220
|
+
queueTopicRewritePhase(stateManager);
|
|
221
|
+
queueAllScans(context, stateManager, {
|
|
222
|
+
extraction_model: human.settings?.codex?.extraction_model,
|
|
223
|
+
external_filter: "only",
|
|
224
|
+
});
|
|
225
|
+
result.extractionScansQueued += 4;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
result.sessionsProcessed = 1;
|
|
229
|
+
updateProcessedState(stateManager, targetSession);
|
|
230
|
+
|
|
231
|
+
console.log(
|
|
232
|
+
`[Codex] Session complete: ${result.messagesImported} messages imported, ` +
|
|
233
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { CodexReader } from "./reader.js";
|
|
2
|
+
export { importCodexSessions } from "./importer.js";
|
|
3
|
+
export type { CodexImportResult, CodexImporterOptions } from "./importer.js";
|
|
4
|
+
export type {
|
|
5
|
+
CodexMessage,
|
|
6
|
+
CodexMessageWindow,
|
|
7
|
+
CodexSession,
|
|
8
|
+
CodexSettings,
|
|
9
|
+
ICodexReader,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
export { CODEX_PERSONA_NAME } from "./types.js";
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CodexMessage,
|
|
3
|
+
CodexMessageWindow,
|
|
4
|
+
CodexRolloutRecord,
|
|
5
|
+
CodexSession,
|
|
6
|
+
CodexThreadRow,
|
|
7
|
+
ICodexReader,
|
|
8
|
+
} from "./types.js";
|
|
9
|
+
|
|
10
|
+
const isBrowser = typeof document !== "undefined";
|
|
11
|
+
|
|
12
|
+
let _join: typeof import("path").join;
|
|
13
|
+
let _basename: typeof import("path").basename;
|
|
14
|
+
let _readFile: typeof import("fs/promises").readFile;
|
|
15
|
+
let _readdir: typeof import("fs/promises").readdir;
|
|
16
|
+
let _stat: typeof import("fs/promises").stat;
|
|
17
|
+
let _nodeModulesLoaded = false;
|
|
18
|
+
|
|
19
|
+
async function ensureNodeModules(): Promise<boolean> {
|
|
20
|
+
if (isBrowser) return false;
|
|
21
|
+
if (_nodeModulesLoaded) return true;
|
|
22
|
+
|
|
23
|
+
const PATH_MODULE = "path";
|
|
24
|
+
const FS_MODULE = "fs/promises";
|
|
25
|
+
|
|
26
|
+
const pathMod = await import(/* @vite-ignore */ PATH_MODULE);
|
|
27
|
+
const fsMod = await import(/* @vite-ignore */ FS_MODULE);
|
|
28
|
+
|
|
29
|
+
_join = pathMod.join;
|
|
30
|
+
_basename = pathMod.basename;
|
|
31
|
+
_readFile = fsMod.readFile;
|
|
32
|
+
_readdir = fsMod.readdir;
|
|
33
|
+
_stat = fsMod.stat;
|
|
34
|
+
_nodeModulesLoaded = true;
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getDefaultCodexHome(): string {
|
|
39
|
+
if (!_join) return "";
|
|
40
|
+
return process.env.CODEX_HOME || _join(process.env.HOME || "~", ".codex");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function titleFromCwd(cwd: string): string {
|
|
44
|
+
if (!cwd) return "Codex Session";
|
|
45
|
+
const parts = cwd.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
46
|
+
return parts[parts.length - 1] ?? cwd;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function timestampFromMs(value: number | null | undefined): string | null {
|
|
50
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return null;
|
|
51
|
+
return new Date(value).toISOString();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function extractEventMessage(record: CodexRolloutRecord, lineIndex: number, sessionId: string): CodexMessage | null {
|
|
55
|
+
if (record.type !== "event_msg") return null;
|
|
56
|
+
|
|
57
|
+
const payload = record.payload ?? {};
|
|
58
|
+
const payloadType = payload.type;
|
|
59
|
+
if (payloadType !== "user_message" && payloadType !== "agent_message") return null;
|
|
60
|
+
|
|
61
|
+
const rawMessage = payload.message;
|
|
62
|
+
if (typeof rawMessage !== "string" || rawMessage.trim() === "") return null;
|
|
63
|
+
|
|
64
|
+
const timestamp = typeof record.timestamp === "string" && record.timestamp.trim()
|
|
65
|
+
? record.timestamp
|
|
66
|
+
: new Date(0).toISOString();
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
id: `evt_${lineIndex + 1}`,
|
|
70
|
+
sessionId,
|
|
71
|
+
role: payloadType === "user_message" ? "user" : "assistant",
|
|
72
|
+
content: rawMessage.trim(),
|
|
73
|
+
timestamp,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function parseCodexRolloutMessages(text: string, sessionId: string): CodexMessage[] {
|
|
78
|
+
const messages: CodexMessage[] = [];
|
|
79
|
+
const lines = text.split("\n");
|
|
80
|
+
|
|
81
|
+
for (let i = 0; i < lines.length; i++) {
|
|
82
|
+
const trimmed = lines[i].trim();
|
|
83
|
+
if (!trimmed) continue;
|
|
84
|
+
|
|
85
|
+
let record: CodexRolloutRecord;
|
|
86
|
+
try {
|
|
87
|
+
record = JSON.parse(trimmed) as CodexRolloutRecord;
|
|
88
|
+
} catch {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const message = extractEventMessage(record, i, sessionId);
|
|
93
|
+
if (message) messages.push(message);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return messages.sort(
|
|
97
|
+
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export class CodexReader implements ICodexReader {
|
|
102
|
+
private readonly codexHome?: string;
|
|
103
|
+
|
|
104
|
+
constructor(codexHome?: string) {
|
|
105
|
+
this.codexHome = codexHome;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async isAvailable(): Promise<boolean> {
|
|
109
|
+
if (!(await ensureNodeModules())) return false;
|
|
110
|
+
const dbPath = await this.findStateDbPath();
|
|
111
|
+
if (!dbPath) return false;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
return (await _stat(dbPath)).isFile();
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async getSessions(): Promise<CodexSession[]> {
|
|
121
|
+
if (!(await ensureNodeModules())) return [];
|
|
122
|
+
|
|
123
|
+
const dbPath = await this.findStateDbPath();
|
|
124
|
+
if (!dbPath) return [];
|
|
125
|
+
|
|
126
|
+
let db: import("bun:sqlite").Database;
|
|
127
|
+
try {
|
|
128
|
+
const { Database } = await import(/* @vite-ignore */ "bun:sqlite");
|
|
129
|
+
db = new Database(dbPath, { readonly: true });
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.warn("[CodexReader] failed to open state DB:", err);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
let rows: CodexThreadRow[];
|
|
136
|
+
try {
|
|
137
|
+
rows = db.query("SELECT * FROM threads WHERE rollout_path IS NOT NULL AND rollout_path != ''").all() as CodexThreadRow[];
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.warn("[CodexReader] failed to read threads table:", err);
|
|
140
|
+
db.close();
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
db.close();
|
|
145
|
+
|
|
146
|
+
const sessions: CodexSession[] = [];
|
|
147
|
+
for (const row of rows) {
|
|
148
|
+
if (!row.id || !row.rollout_path) continue;
|
|
149
|
+
const session = await this.sessionFromThreadRow(row);
|
|
150
|
+
if (session) sessions.push(session);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return sessions.sort(
|
|
154
|
+
(a, b) => new Date(a.lastMessageAt).getTime() - new Date(b.lastMessageAt).getTime()
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async getMessageById(
|
|
159
|
+
sessionId: string,
|
|
160
|
+
messageId: string,
|
|
161
|
+
before = 0,
|
|
162
|
+
after = 0
|
|
163
|
+
): Promise<CodexMessageWindow | null> {
|
|
164
|
+
const sessions = await this.getSessions();
|
|
165
|
+
const session = sessions.find((s) => s.id === sessionId);
|
|
166
|
+
if (!session) return null;
|
|
167
|
+
|
|
168
|
+
const idx = session.messages.findIndex((m) => m.id === messageId);
|
|
169
|
+
if (idx === -1) return null;
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
message: session.messages[idx],
|
|
173
|
+
before: session.messages.slice(Math.max(0, idx - before), idx),
|
|
174
|
+
after: session.messages.slice(idx + 1, idx + 1 + after),
|
|
175
|
+
session,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private async findStateDbPath(): Promise<string | null> {
|
|
180
|
+
const base = this.codexHome ?? getDefaultCodexHome();
|
|
181
|
+
let entries: string[];
|
|
182
|
+
try {
|
|
183
|
+
entries = await _readdir(base);
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const stateDbs = entries
|
|
189
|
+
.map((name) => {
|
|
190
|
+
const match = name.match(/^state_(\d+)\.sqlite$/);
|
|
191
|
+
return match ? { name, version: Number(match[1]) } : null;
|
|
192
|
+
})
|
|
193
|
+
.filter((entry): entry is { name: string; version: number } => entry !== null)
|
|
194
|
+
.sort((a, b) => b.version - a.version);
|
|
195
|
+
|
|
196
|
+
if (stateDbs.length > 0) return _join(base, stateDbs[0].name);
|
|
197
|
+
if (entries.includes("state.sqlite")) return _join(base, "state.sqlite");
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private async sessionFromThreadRow(row: CodexThreadRow): Promise<CodexSession | null> {
|
|
202
|
+
const rolloutPath = row.rollout_path;
|
|
203
|
+
if (!rolloutPath) return null;
|
|
204
|
+
|
|
205
|
+
const messages = await this.readMessages(row.id, rolloutPath);
|
|
206
|
+
if (messages.length === 0) return null;
|
|
207
|
+
|
|
208
|
+
const first = messages[0];
|
|
209
|
+
const last = messages[messages.length - 1];
|
|
210
|
+
const title = row.title?.trim()
|
|
211
|
+
|| row.first_user_message?.trim()
|
|
212
|
+
|| titleFromCwd(row.cwd ?? "");
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
id: row.id,
|
|
216
|
+
title,
|
|
217
|
+
cwd: row.cwd ?? "",
|
|
218
|
+
source: row.source ?? undefined,
|
|
219
|
+
threadSource: row.thread_source ?? undefined,
|
|
220
|
+
agentNickname: row.agent_nickname ?? undefined,
|
|
221
|
+
agentRole: row.agent_role ?? undefined,
|
|
222
|
+
agentPath: row.agent_path ?? undefined,
|
|
223
|
+
rolloutPath,
|
|
224
|
+
firstMessageAt: first.timestamp || timestampFromMs(row.created_at_ms) || timestampFromMs(row.created_at) || new Date(0).toISOString(),
|
|
225
|
+
lastMessageAt: last.timestamp || timestampFromMs(row.updated_at_ms) || timestampFromMs(row.updated_at) || new Date(0).toISOString(),
|
|
226
|
+
messages,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
private async readMessages(sessionId: string, rolloutPath: string): Promise<CodexMessage[]> {
|
|
231
|
+
let text: string;
|
|
232
|
+
try {
|
|
233
|
+
text = await _readFile(rolloutPath, "utf-8");
|
|
234
|
+
} catch (err) {
|
|
235
|
+
console.warn(`[CodexReader] skipping missing rollout ${_basename(rolloutPath)}:`, err);
|
|
236
|
+
return [];
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return parseCodexRolloutMessages(text, sessionId);
|
|
240
|
+
}
|
|
241
|
+
}
|