ei-tui 1.6.0 → 1.6.2
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 +9 -7
- package/src/cli/mcp.ts +3 -3
- package/src/cli/retrieval.ts +22 -0
- package/src/cli.ts +213 -13
- package/src/core/context-utils.ts +0 -1
- package/src/core/orchestrators/ceremony.ts +48 -17
- package/src/core/processor.ts +92 -3
- package/src/core/prompt-context-builder.ts +2 -0
- package/src/core/tools/builtin/persona-notes.ts +81 -0
- package/src/core/tools/index.ts +56 -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 +16 -0
- package/src/integrations/codex/importer.ts +258 -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/opencode/reader-factory.ts +4 -4
- package/src/integrations/slack/importer.ts +0 -1
- package/src/prompts/response/index.ts +5 -2
- package/src/prompts/response/sections.ts +10 -0
- package/src/prompts/response/types.ts +1 -0
- package/src/prompts/room/index.ts +3 -0
- package/src/prompts/room/types.ts +1 -0
- package/tui/README.md +4 -3
- package/tui/src/util/yaml-settings.ts +28 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { StateManager } from "../../core/state-manager.js";
|
|
2
|
+
import type { ContextStatus, 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 { getMachineId } from "../machine-id.js";
|
|
14
|
+
import { isProcessRunning } from "../process-check.js";
|
|
15
|
+
import { CodexReader } from "./reader.js";
|
|
16
|
+
import {
|
|
17
|
+
CODEX_PERSONA_NAME,
|
|
18
|
+
MIN_SESSION_AGE_MS,
|
|
19
|
+
type CodexMessage,
|
|
20
|
+
type CodexSession,
|
|
21
|
+
type ICodexReader,
|
|
22
|
+
} from "./types.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 TWELVE_HOURS_MS = 43_200_000;
|
|
39
|
+
const CODEX_GROUP = "Codex";
|
|
40
|
+
|
|
41
|
+
function convertToEiMessage(msg: CodexMessage, sessionId: string): Message {
|
|
42
|
+
return {
|
|
43
|
+
id: qualifyCodexMessage(getMachineId(), sessionId, msg.id),
|
|
44
|
+
role: msg.role === "user" ? "human" : "system",
|
|
45
|
+
content: msg.content,
|
|
46
|
+
timestamp: msg.timestamp,
|
|
47
|
+
read: true,
|
|
48
|
+
context_status: "default" as ContextStatus,
|
|
49
|
+
external: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function convertToPreMarkedEiMessage(msg: CodexMessage, sessionId: string): Message {
|
|
54
|
+
return {
|
|
55
|
+
...convertToEiMessage(msg, sessionId),
|
|
56
|
+
f: true,
|
|
57
|
+
t: true,
|
|
58
|
+
p: true,
|
|
59
|
+
e: true,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function ensureCodexPersona(
|
|
64
|
+
stateManager: StateManager,
|
|
65
|
+
eiInterface?: Ei_Interface
|
|
66
|
+
): PersonaEntity {
|
|
67
|
+
const existing = stateManager.persona_getByName(CODEX_PERSONA_NAME);
|
|
68
|
+
if (existing) return existing;
|
|
69
|
+
|
|
70
|
+
const now = new Date().toISOString();
|
|
71
|
+
const seedTraits: PersonaTrait[] = DEFAULT_SEED_TRAITS.map((t) => ({
|
|
72
|
+
id: crypto.randomUUID(),
|
|
73
|
+
name: t.name,
|
|
74
|
+
description: t.description,
|
|
75
|
+
sentiment: t.sentiment,
|
|
76
|
+
strength: t.strength,
|
|
77
|
+
last_updated: now,
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
const persona: PersonaEntity = {
|
|
81
|
+
id: crypto.randomUUID(),
|
|
82
|
+
display_name: CODEX_PERSONA_NAME,
|
|
83
|
+
entity: "system",
|
|
84
|
+
aliases: ["codex", "codex cli", "codex desktop", "openai codex"],
|
|
85
|
+
short_description: "Codex - OpenAI coding agent environment",
|
|
86
|
+
long_description:
|
|
87
|
+
"Codex is OpenAI's coding agent environment for working with local codebases, terminal commands, tools, and implementation tasks.",
|
|
88
|
+
group_primary: CODEX_GROUP,
|
|
89
|
+
groups_visible: [CODEX_GROUP],
|
|
90
|
+
traits: seedTraits,
|
|
91
|
+
topics: [],
|
|
92
|
+
is_paused: false,
|
|
93
|
+
is_archived: false,
|
|
94
|
+
is_static: false,
|
|
95
|
+
heartbeat_delay_ms: TWELVE_HOURS_MS,
|
|
96
|
+
last_heartbeat: now,
|
|
97
|
+
last_updated: now,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
stateManager.persona_add(persona);
|
|
101
|
+
eiInterface?.onPersonaAdded?.();
|
|
102
|
+
return persona;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function updateProcessedState(
|
|
106
|
+
stateManager: StateManager,
|
|
107
|
+
session: CodexSession
|
|
108
|
+
): void {
|
|
109
|
+
const human = stateManager.getHuman();
|
|
110
|
+
const lastMessageMs = new Date(session.lastMessageAt).getTime();
|
|
111
|
+
const extractionPoint = human.settings?.codex?.extraction_point;
|
|
112
|
+
const currentPointMs = extractionPoint ? new Date(extractionPoint).getTime() : 0;
|
|
113
|
+
const newPointMs = Math.max(currentPointMs, lastMessageMs);
|
|
114
|
+
|
|
115
|
+
const processedSessions = {
|
|
116
|
+
...(human.settings?.codex?.processed_sessions ?? {}),
|
|
117
|
+
[session.id]: new Date().toISOString(),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
stateManager.setHuman({
|
|
121
|
+
...human,
|
|
122
|
+
settings: {
|
|
123
|
+
...human.settings,
|
|
124
|
+
codex: {
|
|
125
|
+
...human.settings?.codex,
|
|
126
|
+
extraction_point: new Date(newPointMs).toISOString(),
|
|
127
|
+
processed_sessions: processedSessions,
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async function isCodexRunning(): Promise<boolean> {
|
|
134
|
+
return (await isProcessRunning("Codex")) || (await isProcessRunning("codex"));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export async function importCodexSessions(
|
|
138
|
+
options: CodexImporterOptions
|
|
139
|
+
): Promise<CodexImportResult> {
|
|
140
|
+
const { stateManager, interface: eiInterface, signal } = options;
|
|
141
|
+
const reader = options.reader ?? new CodexReader();
|
|
142
|
+
|
|
143
|
+
const result: CodexImportResult = {
|
|
144
|
+
sessionsProcessed: 0,
|
|
145
|
+
messagesImported: 0,
|
|
146
|
+
personaCreated: false,
|
|
147
|
+
extractionScansQueued: 0,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const allSessions = await reader.getSessions();
|
|
151
|
+
if (signal?.aborted) return result;
|
|
152
|
+
|
|
153
|
+
const human = stateManager.getHuman();
|
|
154
|
+
const processedSessions = human.settings?.codex?.processed_sessions ?? {};
|
|
155
|
+
const now = Date.now();
|
|
156
|
+
const toolRunning = await isCodexRunning();
|
|
157
|
+
|
|
158
|
+
let targetSession: CodexSession | null = null;
|
|
159
|
+
|
|
160
|
+
for (const session of allSessions) {
|
|
161
|
+
const sessionLastMs = new Date(session.lastMessageAt).getTime();
|
|
162
|
+
const ageMs = now - sessionLastMs;
|
|
163
|
+
|
|
164
|
+
if (ageMs < MIN_SESSION_AGE_MS && toolRunning) continue;
|
|
165
|
+
|
|
166
|
+
const lastImported = processedSessions[session.id];
|
|
167
|
+
if (lastImported && sessionLastMs <= new Date(lastImported).getTime()) continue;
|
|
168
|
+
|
|
169
|
+
targetSession = session;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!targetSession) {
|
|
174
|
+
console.log("[Codex] All sessions processed, nothing new to import");
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (signal?.aborted) return result;
|
|
179
|
+
|
|
180
|
+
console.log(
|
|
181
|
+
`[Codex] Processing session: "${targetSession.title}" ` +
|
|
182
|
+
`(last message: ${targetSession.lastMessageAt})`
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
const messages = targetSession.messages;
|
|
186
|
+
if (messages.length === 0) {
|
|
187
|
+
updateProcessedState(stateManager, targetSession);
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (signal?.aborted) return result;
|
|
192
|
+
|
|
193
|
+
const personaExistedBefore = stateManager.persona_getByName(CODEX_PERSONA_NAME) !== null;
|
|
194
|
+
const persona = ensureCodexPersona(stateManager, eiInterface);
|
|
195
|
+
result.personaCreated = !personaExistedBefore;
|
|
196
|
+
|
|
197
|
+
if (!personaExistedBefore) {
|
|
198
|
+
stateManager.persona_archive(persona.id);
|
|
199
|
+
} else {
|
|
200
|
+
const existingMsgs = stateManager.messages_get(persona.id);
|
|
201
|
+
const externalIds = existingMsgs.filter((m) => m.external === true).map((m) => m.id);
|
|
202
|
+
if (externalIds.length > 0) {
|
|
203
|
+
stateManager.messages_remove(persona.id, externalIds);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const cutoffIso = processedSessions[targetSession.id] ?? null;
|
|
208
|
+
const cutoffMs = cutoffIso ? new Date(cutoffIso).getTime() : null;
|
|
209
|
+
const toAnalyze: Message[] = [];
|
|
210
|
+
|
|
211
|
+
for (const msg of messages) {
|
|
212
|
+
const msgMs = new Date(msg.timestamp).getTime();
|
|
213
|
+
const isOld = cutoffMs !== null && msgMs < cutoffMs;
|
|
214
|
+
const eiMsg = isOld
|
|
215
|
+
? convertToPreMarkedEiMessage(msg, targetSession.id)
|
|
216
|
+
: convertToEiMessage(msg, targetSession.id);
|
|
217
|
+
|
|
218
|
+
stateManager.messages_append(persona.id, eiMsg);
|
|
219
|
+
result.messagesImported++;
|
|
220
|
+
if (!isOld) toAnalyze.push(eiMsg);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
stateManager.messages_sort(persona.id);
|
|
224
|
+
eiInterface?.onMessageAdded?.(persona.id);
|
|
225
|
+
|
|
226
|
+
if (toAnalyze.length > 0 && !signal?.aborted) {
|
|
227
|
+
const allInState = stateManager.messages_get(persona.id);
|
|
228
|
+
const analyzeIds = new Set(toAnalyze.map((m) => m.id));
|
|
229
|
+
const analyzeStartIndex = allInState.findIndex((m) => analyzeIds.has(m.id));
|
|
230
|
+
const contextMsgs = analyzeStartIndex > 0 ? allInState.slice(0, analyzeStartIndex) : [];
|
|
231
|
+
|
|
232
|
+
const context: ExtractionContext = {
|
|
233
|
+
personaId: persona.id,
|
|
234
|
+
channelDisplayName: persona.display_name,
|
|
235
|
+
messages_context: contextMsgs,
|
|
236
|
+
messages_analyze: toAnalyze,
|
|
237
|
+
sources: [`codex:${getMachineId()}:${targetSession.id}`],
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
queuePersonRewritePhase(stateManager);
|
|
241
|
+
queueTopicRewritePhase(stateManager);
|
|
242
|
+
queueAllScans(context, stateManager, {
|
|
243
|
+
extraction_model: human.settings?.codex?.extraction_model,
|
|
244
|
+
external_filter: "only",
|
|
245
|
+
});
|
|
246
|
+
result.extractionScansQueued += 4;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
result.sessionsProcessed = 1;
|
|
250
|
+
updateProcessedState(stateManager, targetSession);
|
|
251
|
+
|
|
252
|
+
console.log(
|
|
253
|
+
`[Codex] Session complete: ${result.messagesImported} messages imported, ` +
|
|
254
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
return result;
|
|
258
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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 const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
|
|
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
|
+
}
|
|
@@ -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.
|
|
14
|
+
console.error("[OpenCode] Using SQLite reader");
|
|
15
15
|
return new SqliteReader(dbPath);
|
|
16
16
|
} catch {
|
|
17
|
-
console.
|
|
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.
|
|
22
|
+
console.error("[OpenCode] Using JSON reader (legacy)");
|
|
23
23
|
return new JsonReader(storagePath);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
console.
|
|
26
|
+
console.error("[OpenCode] No OpenCode data found");
|
|
27
27
|
return new JsonReader(storagePath);
|
|
28
28
|
}
|
|
29
29
|
|
|
@@ -27,7 +27,6 @@ export interface SlackImportResult {
|
|
|
27
27
|
function ensureSlackPersona(stateManager: StateManager, eiInterface: Ei_Interface): PersonaEntity {
|
|
28
28
|
const existing = stateManager.persona_getAll().find(p => p.display_name === "Slack");
|
|
29
29
|
if (existing) {
|
|
30
|
-
if (existing.is_archived) stateManager.persona_unarchive(existing.id);
|
|
31
30
|
return existing;
|
|
32
31
|
}
|
|
33
32
|
const persona: PersonaEntity = {
|
|
@@ -11,6 +11,7 @@ import type { ResponsePromptData, PromptOutput } from "./types.js";
|
|
|
11
11
|
import { formatCurrentTime } from "../../core/format-utils.js";
|
|
12
12
|
import {
|
|
13
13
|
buildIdentitySection,
|
|
14
|
+
buildNotesSection,
|
|
14
15
|
buildGuidelinesSection,
|
|
15
16
|
buildTraitsSection,
|
|
16
17
|
buildTopicsSection,
|
|
@@ -44,6 +45,7 @@ Your role is unique among personas:
|
|
|
44
45
|
- Consider their traits when building your responses more than the current conversation history
|
|
45
46
|
- You encourage human-to-human connection when appropriate`;
|
|
46
47
|
|
|
48
|
+
const notesSection = buildNotesSection(data.persona.notes);
|
|
47
49
|
const guidelines = buildGuidelinesSection("ei");
|
|
48
50
|
const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
|
|
49
51
|
const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
|
|
@@ -63,7 +65,7 @@ Your role is unique among personas:
|
|
|
63
65
|
: "";
|
|
64
66
|
|
|
65
67
|
return `${identity}
|
|
66
|
-
|
|
68
|
+
${notesSection ? `\n${notesSection}` : ""}
|
|
67
69
|
${guidelines}
|
|
68
70
|
|
|
69
71
|
${yourTraits}
|
|
@@ -93,6 +95,7 @@ ${conversationState}
|
|
|
93
95
|
*/
|
|
94
96
|
function buildStandardSystemPrompt(data: ResponsePromptData): string {
|
|
95
97
|
const identity = buildIdentitySection(data.persona);
|
|
98
|
+
const notesSection = buildNotesSection(data.persona.notes);
|
|
96
99
|
const guidelines = buildGuidelinesSection(data.persona.name);
|
|
97
100
|
const yourTraits = buildTraitsSection(data.persona.traits, "Your Personality");
|
|
98
101
|
const yourTopics = buildTopicsSection(data.persona.topics, "Your Interests");
|
|
@@ -111,7 +114,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
|
|
|
111
114
|
: "";
|
|
112
115
|
|
|
113
116
|
return `${identity}
|
|
114
|
-
|
|
117
|
+
${notesSection ? `\n${notesSection}` : ""}
|
|
115
118
|
${guidelines}
|
|
116
119
|
|
|
117
120
|
${yourTraits}
|