ei-tui 1.6.1 → 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 +210 -11
- package/src/core/context-utils.ts +0 -1
- package/src/core/orchestrators/ceremony.ts +1 -1
- package/src/core/processor.ts +81 -0
- package/src/core/types/data-items.ts +1 -1
- package/src/core/types/entities.ts +1 -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/tui/README.md +4 -3
- package/tui/src/util/yaml-settings.ts +28 -0
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Ei
|
|
2
2
|
|
|
3
|
-
A local-first AI companion system with persistent personas and coding tool integrations (OpenCode, Claude Code, Cursor).
|
|
3
|
+
A local-first AI companion system with persistent personas and coding tool integrations (OpenCode, Claude Code, Cursor, Codex).
|
|
4
4
|
|
|
5
5
|
You can access the Web version at [ei.flare576.com](https://ei.flare576.com).
|
|
6
6
|
|
|
7
7
|
You can run the local version via `bunx ei-tui` — no install needed, always current (see [### TUI](#tui) for details).
|
|
8
8
|
|
|
9
|
-
If you're here to give your coding tools (OpenCode, Claude Code, Cursor) persistent memory, jump over to [TUI README.md](./tui/README.md) to learn how to get information _into_ Ei, and [CLI README.md](./src/cli/README.md) to wire up
|
|
9
|
+
If you're here to give your coding tools (OpenCode, Claude Code, Cursor, Codex) persistent memory, jump over to [TUI README.md](./tui/README.md) to learn how to get information _into_ Ei, and [CLI README.md](./src/cli/README.md) to wire up MCP/context injection so your agents can read relevant memory when they need it.
|
|
10
10
|
|
|
11
11
|
## What Does "Local First" Mean?
|
|
12
12
|
|
|
@@ -83,7 +83,7 @@ Ei can operate with three types of input, and three types of output.
|
|
|
83
83
|
^
|
|
84
84
|
Sessions
|
|
85
85
|
|
|
|
86
|
-
[OpenCode / Claude Code / Cursor]
|
|
86
|
+
[OpenCode / Claude Code / Cursor / Codex]
|
|
87
87
|
```
|
|
88
88
|
|
|
89
89
|
```
|
|
@@ -91,7 +91,7 @@ Ei can operate with three types of input, and three types of output.
|
|
|
91
91
|
|
|
|
92
92
|
CLI Data
|
|
93
93
|
v
|
|
94
|
-
|
|
94
|
+
[OpenCode / Claude Code / Cursor / Codex]
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
Optionally, users can opt into a server-side data sync. This is ideal for users who want to use multiple devices or switch between TUI and Web throughout the day. All data is encrypted _before_ being sent to the server, using a key that only the user can generate (your `username` and `passphrase` never leave your device - I couldn't decrypt your data if I wanted to).
|
|
@@ -129,7 +129,7 @@ More information (including commands) can be found in the [TUI Readme](tui/READM
|
|
|
129
129
|
|
|
130
130
|
Ei can import sessions from your coding tools and extract what you've been working on — pulling out facts, topics, and context that persist across sessions. Enable any combination; they work independently and feed into the same knowledge base.
|
|
131
131
|
|
|
132
|
-
All
|
|
132
|
+
All four integrations are enabled via `/settings` in the TUI.
|
|
133
133
|
|
|
134
134
|
#### OpenCode
|
|
135
135
|
|
|
@@ -165,6 +165,21 @@ Reads from Cursor's SQLite databases:
|
|
|
165
165
|
|
|
166
166
|
All sessions map to a single "Cursor" persona.
|
|
167
167
|
|
|
168
|
+
#### Codex
|
|
169
|
+
|
|
170
|
+
```yaml
|
|
171
|
+
codex:
|
|
172
|
+
integration: true
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Reads from Codex's local state database and rollout JSONL files:
|
|
176
|
+
- `~/.codex/state_*.sqlite`
|
|
177
|
+
- `~/.codex/sessions/`
|
|
178
|
+
|
|
179
|
+
All sessions map to a single "Codex" persona. Codex may store thread-level agent metadata for custom agents, but rollout messages do not reliably expose per-message sub-agent speaker identity yet. Tool calls, prompt scaffolding, and token-count events are stripped — only visible user/agent messages are imported.
|
|
180
|
+
|
|
181
|
+
Codex can also read Ei's knowledge back out. Run `ei --install` to register the Ei MCP server and install a Codex `UserPromptSubmit` hook that injects relevant memory before each message, using your current prompt plus recent Codex transcript context when available.
|
|
182
|
+
|
|
168
183
|
---
|
|
169
184
|
|
|
170
185
|
Sessions are processed oldest-first, one per queue cycle, so Ei won't overwhelm your LLM provider on first run. See [TUI Readme](tui/README.md)
|
|
@@ -233,7 +248,7 @@ Personas can use tools. Not just read-from-memory tools — *actual* tools. Web
|
|
|
233
248
|
|------|-------------|
|
|
234
249
|
| `find_memory` | Semantic search of your personal memory — facts, traits, topics, people, quotes. Personas call this automatically when the conversation touches something they might know about you. Supports the `persona` filter to scope results to what a specific persona has learned. |
|
|
235
250
|
| `fetch_memory` | Full-record lookup for a specific human entity (Fact, Topic, Person, or Quote) by ID. Use after `find_memory` to retrieve complete details. |
|
|
236
|
-
| `fetch_message` | Retrieve a specific message by ID with optional surrounding context. Searches
|
|
251
|
+
| `fetch_message` | Retrieve a specific message by fully-qualified ID with optional surrounding context. Searches Ei conversations and supported external coding-session stores. |
|
|
237
252
|
| `file_read` | Read a file from your local filesystem *(TUI only)* |
|
|
238
253
|
| `list_directory` | Explore folder structure *(TUI only)* |
|
|
239
254
|
| `directory_tree` | Recursive directory tree *(TUI only)* |
|
package/package.json
CHANGED
package/src/cli/README.md
CHANGED
|
@@ -15,8 +15,8 @@ ei --recent # Most recently mentioned items (no query
|
|
|
15
15
|
ei --persona "Beta" --recent # Most recently mentioned items Beta has learned
|
|
16
16
|
ei --id <id> # Look up entity by ID — or fetch a message by FQ ID
|
|
17
17
|
echo <id> | ei --id # Look up entity by ID from stdin
|
|
18
|
-
ei --install # Wire Ei into Claude Code, Cursor, and OpenCode (MCP + hooks + persona plugin)
|
|
19
|
-
ei mcp # Start the Ei MCP stdio server (for Cursor/
|
|
18
|
+
ei --install # Wire Ei into Claude Code, Cursor, Codex, and OpenCode (MCP + hooks + persona plugin)
|
|
19
|
+
ei mcp # Start the Ei MCP stdio server (for Claude Code/Cursor/Codex)
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
Type aliases: `fact`, `person`, `topic`, `quote`, `persona` all work (singular or plural).
|
|
@@ -35,6 +35,7 @@ It also resolves fully-qualified message IDs from any supported integration, ret
|
|
|
35
35
|
ei --id "opencode:jeremys-macbook-pro:ses_38a7...:msg_c75b..."
|
|
36
36
|
ei --id "claudecode:my-machine:session-uuid:message-uuid"
|
|
37
37
|
ei --id "cursor:my-machine:composer-uuid:bubble-uuid"
|
|
38
|
+
ei --id "codex:my-machine:thread-uuid:evt_42"
|
|
38
39
|
```
|
|
39
40
|
|
|
40
41
|
Quotes surfaced by `ei_search` include a `message_id` field in this format — pipe it to `ei --id` to read the original conversation.
|
|
@@ -47,15 +48,16 @@ Quotes surfaced by `ei_search` include a `message_id` field in this format — p
|
|
|
47
48
|
ei --install
|
|
48
49
|
```
|
|
49
50
|
|
|
50
|
-
This registers Ei with Claude Code, Cursor, and OpenCode — MCP server config, context injection hooks, and (for OpenCode) a persona identity plugin so agents know who they are before the first message:
|
|
51
|
+
This registers Ei with Claude Code, Cursor, Codex, and OpenCode — MCP server config, context injection hooks where supported, and (for OpenCode) a persona identity plugin so agents know who they are before the first message:
|
|
51
52
|
|
|
52
53
|
| Tool | MCP | Context Hook | Persona Plugin |
|
|
53
54
|
|------|-----|-------------|----------------|
|
|
54
55
|
| **Claude Code** | `~/.claude.json` | `~/.claude/settings.json` (`UserPromptSubmit`) + `~/.claude/hooks/ei-inject.ts` | — |
|
|
55
56
|
| **Cursor** | `~/.cursor/mcp.json` | `~/.cursor/hooks.json` (`beforeSubmitPrompt`) + `~/.cursor/hooks/ei-inject.sh` | — |
|
|
57
|
+
| **Codex** | `~/.codex/config.toml` via `codex mcp add ei` | `~/.codex/hooks.json` (`UserPromptSubmit`) + `~/.codex/hooks/ei-inject.ts` | Local Codex agent plugin if installed separately |
|
|
56
58
|
| **OpenCode** | manual (see below) | Via Oh My OpenCode compatibility layer (reads `~/.claude/settings.json`) | `~/.config/opencode/plugins/ei-persona.ts` |
|
|
57
59
|
|
|
58
|
-
**Context hook**: fires before every message, searches Ei for
|
|
60
|
+
**Context hook**: fires before every message, searches Ei for relevant memory, and injects it silently. No tool call required.
|
|
59
61
|
|
|
60
62
|
**Persona plugin** (OpenCode + [Oh My OpenCode](https://github.com/code-yeongyu/oh-my-opencode) only): injects the agent's Ei relationship record directly into the system prompt at session start — traits, working style, shared context. The agent knows who it is *to you* before it reads a word of your message.
|
|
61
63
|
|
|
@@ -80,7 +82,7 @@ Restart your agent tool after changes to activate.
|
|
|
80
82
|
|
|
81
83
|
### MCP Server
|
|
82
84
|
|
|
83
|
-
Claude Code and
|
|
85
|
+
Claude Code, Cursor, and Codex call `ei mcp` to start the MCP stdio server. You can run it directly to test:
|
|
84
86
|
|
|
85
87
|
```sh
|
|
86
88
|
ei mcp
|
|
@@ -97,7 +99,7 @@ The `ei_search`, `ei_lookup`, and `ei_fetch_message` MCP tools are still availab
|
|
|
97
99
|
|
|
98
100
|
## MCP Tools Reference
|
|
99
101
|
|
|
100
|
-
The MCP server exposes these tools to Claude Code, Cursor, and OpenCode:
|
|
102
|
+
The MCP server exposes these tools to Claude Code, Cursor, Codex, and OpenCode:
|
|
101
103
|
|
|
102
104
|
| Tool | Description |
|
|
103
105
|
|------|-------------|
|
|
@@ -112,7 +114,7 @@ The MCP server exposes these tools to Claude Code, Cursor, and OpenCode:
|
|
|
112
114
|
| `query` | string (optional) | Search text. Omit to browse by recency. |
|
|
113
115
|
| `type` | enum (optional) | `facts` \| `people` \| `topics` \| `quotes` \| `personas` — omit for balanced results across all types |
|
|
114
116
|
| `persona` | string (optional) | Persona display_name to scope results to what that persona has learned |
|
|
115
|
-
| `source` | string (optional) | Prefix match against source identifiers (e.g. `opencode`, `cursor:my-machine`) |
|
|
117
|
+
| `source` | string (optional) | Prefix match against source identifiers (e.g. `opencode`, `cursor:my-machine`, `codex:my-machine`) |
|
|
116
118
|
| `limit` | number (optional) | Max results, default 10 |
|
|
117
119
|
| `recent` | boolean (optional) | Sort by most recently mentioned instead of relevance |
|
|
118
120
|
|
package/src/cli/mcp.ts
CHANGED
|
@@ -37,7 +37,7 @@ export function createMcpServer(): McpServer {
|
|
|
37
37
|
.string()
|
|
38
38
|
.optional()
|
|
39
39
|
.describe(
|
|
40
|
-
"Filter to entities from a specific source. Prefix match against namespaced source identifiers (e.g. 'cursor', 'opencode', 'opencode:my-machine', '
|
|
40
|
+
"Filter to entities from a specific source. Prefix match against namespaced source identifiers (e.g. 'cursor', 'codex', 'opencode', 'opencode:my-machine', 'codex:my-machine:thread-id')."
|
|
41
41
|
),
|
|
42
42
|
limit: z
|
|
43
43
|
.number()
|
|
@@ -106,7 +106,7 @@ export function createMcpServer(): McpServer {
|
|
|
106
106
|
.string()
|
|
107
107
|
.optional()
|
|
108
108
|
.describe(
|
|
109
|
-
"Filter to entities from a specific source. Prefix match against namespaced source identifiers (e.g. 'cursor', 'opencode', 'opencode:my-machine', '
|
|
109
|
+
"Filter to entities from a specific source. Prefix match against namespaced source identifiers (e.g. 'cursor', 'codex', 'opencode', 'opencode:my-machine', 'codex:my-machine:thread-id'). If the entity does not match, returns not found."
|
|
110
110
|
),
|
|
111
111
|
},
|
|
112
112
|
},
|
|
@@ -134,7 +134,7 @@ export function createMcpServer(): McpServer {
|
|
|
134
134
|
"ei_fetch_message",
|
|
135
135
|
{
|
|
136
136
|
description:
|
|
137
|
-
"Retrieve a specific message by its fully-qualified ID, with optional surrounding conversation context. Use when ei_search returns a quote with a message_id and you want to read the original exchange. The 'before' and 'after' parameters expand the context window in either direction (default 0). Accepts IDs from any integrated source: 'ei:uuid' searches Ei state, 'opencode:machine:session:id' queries OpenCode SQLite, 'claudecode:...' scans Claude Code JSONL files, 'cursor:...' reads the Cursor DB.",
|
|
137
|
+
"Retrieve a specific message by its fully-qualified ID, with optional surrounding conversation context. Use when ei_search returns a quote with a message_id and you want to read the original exchange. The 'before' and 'after' parameters expand the context window in either direction (default 0). Accepts IDs from any integrated source: 'ei:uuid' searches Ei state, 'opencode:machine:session:id' queries OpenCode SQLite, 'claudecode:...' scans Claude Code JSONL files, 'cursor:...' reads the Cursor DB, and 'codex:...' reads Codex rollout history.",
|
|
138
138
|
inputSchema: {
|
|
139
139
|
id: z.string().describe("The ID of the message to retrieve"),
|
|
140
140
|
before: z
|
package/src/cli/retrieval.ts
CHANGED
|
@@ -499,6 +499,28 @@ export async function resolveExternalMessage(
|
|
|
499
499
|
}
|
|
500
500
|
}
|
|
501
501
|
|
|
502
|
+
case "codex": {
|
|
503
|
+
if (parsed.machine !== getMachineId()) {
|
|
504
|
+
return { error: `Message is from machine '${parsed.machine}', not available on this machine (${getMachineId()})` };
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const { CodexReader } = await import("../integrations/codex/reader.js");
|
|
508
|
+
const reader = new CodexReader();
|
|
509
|
+
const win = await reader.getMessageById(parsed.session!, parsed.nativeId, before, after);
|
|
510
|
+
if (!win) return null;
|
|
511
|
+
return {
|
|
512
|
+
type: "opencode_message",
|
|
513
|
+
message: { id: win.message.id, role: win.message.role, content: win.message.content, timestamp: win.message.timestamp },
|
|
514
|
+
before: win.before.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp })),
|
|
515
|
+
after: win.after.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp })),
|
|
516
|
+
session: { id: win.session.id, title: win.session.title, directory: win.session.cwd },
|
|
517
|
+
source: "codex",
|
|
518
|
+
};
|
|
519
|
+
} catch {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
502
524
|
case "unknown":
|
|
503
525
|
default: {
|
|
504
526
|
// Backward compat: bare msg_xxx → treat as opencode (no machine qualifier)
|
package/src/cli.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
/**
|
|
3
|
-
* EI CLI - Memory retrieval interface for
|
|
3
|
+
* EI CLI - Memory retrieval interface for coding tool integrations
|
|
4
4
|
*
|
|
5
5
|
* Usage:
|
|
6
6
|
* ei "search text" Search all data types
|
|
@@ -53,7 +53,7 @@ Usage:
|
|
|
53
53
|
ei --persona "Name" "query" Filter results to what a persona has learned
|
|
54
54
|
ei --id <id> Look up a specific entity by ID
|
|
55
55
|
echo <id> | ei --id Look up entity by ID from stdin
|
|
56
|
-
ei mcp Start the Ei MCP stdio server (for Cursor/
|
|
56
|
+
ei mcp Start the Ei MCP stdio server (for Claude Code/Cursor/Codex)
|
|
57
57
|
|
|
58
58
|
Types:
|
|
59
59
|
quote / quotes Quotes from conversation history
|
|
@@ -66,11 +66,11 @@ Options:
|
|
|
66
66
|
--number, -n Maximum number of results (default: 10)
|
|
67
67
|
--recent, -r Sort by last_mentioned date (most recent first)
|
|
68
68
|
--persona, -p Filter to entities a specific persona has learned about
|
|
69
|
-
--source, -s Filter to entities from a specific source (prefix match, e.g. "cursor", "
|
|
69
|
+
--source, -s Filter to entities from a specific source (prefix match, e.g. "cursor", "codex:my-machine", "opencode:my-machine:ses_abc123")
|
|
70
70
|
--id Look up entity by ID (accepts value or stdin)
|
|
71
|
-
--install Register Ei with Claude Code, Cursor, and OpenCode (MCP + context hooks)
|
|
71
|
+
--install Register Ei with Claude Code, Cursor, Codex, and OpenCode (MCP + context hooks where supported)
|
|
72
72
|
--session <id> Session ID to enrich the query with recent context (use with --hook-source)
|
|
73
|
-
--hook-source <src> Source of the hook: "opencode-plugin" (OpenCode SQLite) or "
|
|
73
|
+
--hook-source <src> Source of the hook: "opencode-plugin" (OpenCode SQLite), "cursor", or "codex"
|
|
74
74
|
--transcript <path> Path to a Claude Code JSONL transcript file for context enrichment
|
|
75
75
|
--help, -h Show this help message
|
|
76
76
|
|
|
@@ -93,6 +93,12 @@ async function installMcpClients(): Promise<void> {
|
|
|
93
93
|
|
|
94
94
|
const home = process.env.HOME || "~";
|
|
95
95
|
|
|
96
|
+
if (await commandExists("codex")) {
|
|
97
|
+
await installCodex();
|
|
98
|
+
} else {
|
|
99
|
+
console.log(`ℹ️ Codex CLI not detected — skipping Codex MCP install.`);
|
|
100
|
+
}
|
|
101
|
+
|
|
96
102
|
const cursorDataDirs = [
|
|
97
103
|
join(home, "Library", "Application Support", "Cursor"),
|
|
98
104
|
join(home, ".config", "Cursor"),
|
|
@@ -117,6 +123,169 @@ async function installMcpClients(): Promise<void> {
|
|
|
117
123
|
}
|
|
118
124
|
}
|
|
119
125
|
|
|
126
|
+
async function commandExists(command: string): Promise<boolean> {
|
|
127
|
+
try {
|
|
128
|
+
const proc = Bun.spawn([command, "--version"], {
|
|
129
|
+
stdout: "ignore",
|
|
130
|
+
stderr: "ignore",
|
|
131
|
+
});
|
|
132
|
+
await proc.exited;
|
|
133
|
+
return proc.exitCode === 0;
|
|
134
|
+
} catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hookEntryHasCommand(entry: unknown, command: string): boolean {
|
|
140
|
+
if (typeof entry !== "object" || entry === null || !("hooks" in entry)) return false;
|
|
141
|
+
const hooks = (entry as { hooks?: unknown }).hooks;
|
|
142
|
+
if (!Array.isArray(hooks)) return false;
|
|
143
|
+
|
|
144
|
+
return hooks.some((hook) => {
|
|
145
|
+
if (typeof hook !== "object" || hook === null) return false;
|
|
146
|
+
const candidate = hook as { type?: unknown; command?: unknown };
|
|
147
|
+
return candidate.type === "command" && candidate.command === command;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function installCodex(): Promise<void> {
|
|
152
|
+
const dataPath = process.env.EI_DATA_PATH ?? join(process.env.HOME || "~", ".local", "share", "ei");
|
|
153
|
+
const proc = Bun.spawn(
|
|
154
|
+
["codex", "mcp", "add", "ei", "--env", `EI_DATA_PATH=${dataPath}`, "--", "bunx", "ei-tui", "mcp"],
|
|
155
|
+
{
|
|
156
|
+
stdout: "pipe",
|
|
157
|
+
stderr: "pipe",
|
|
158
|
+
}
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
162
|
+
new Response(proc.stdout).text(),
|
|
163
|
+
new Response(proc.stderr).text(),
|
|
164
|
+
proc.exited,
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
if (exitCode !== 0) {
|
|
168
|
+
console.warn(`⚠️ Codex MCP install failed.`);
|
|
169
|
+
const detail = (stderr || stdout).trim();
|
|
170
|
+
if (detail) console.warn(` ${detail}`);
|
|
171
|
+
} else {
|
|
172
|
+
console.log(`✓ Installed Ei MCP server to Codex config (~/.codex/config.toml)`);
|
|
173
|
+
console.log(` Restart Codex to activate MCP.`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
await installCodexHooks();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function installCodexHooks(): Promise<void> {
|
|
180
|
+
const home = process.env.HOME || "~";
|
|
181
|
+
const hooksDir = join(home, ".codex", "hooks");
|
|
182
|
+
const scriptPath = join(hooksDir, "ei-inject.ts");
|
|
183
|
+
const hooksJsonPath = join(home, ".codex", "hooks.json");
|
|
184
|
+
|
|
185
|
+
await Bun.$`mkdir -p ${hooksDir}`;
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
await Bun.$`test -w ${hooksDir}`.quiet();
|
|
189
|
+
} catch {
|
|
190
|
+
console.warn(`⚠️ Cannot write to ${hooksDir} (permission denied).`);
|
|
191
|
+
console.warn(` Fix with: sudo chown ${process.env.USER ?? "$(whoami)"} ${hooksDir}`);
|
|
192
|
+
console.warn(` Then re-run: ei --install`);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const scriptContent = `#!/usr/bin/env bun
|
|
197
|
+
import { $ } from "bun";
|
|
198
|
+
|
|
199
|
+
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
200
|
+
const raw = (input.prompt ?? "").replace(/<[^>]*>/g, "").trim();
|
|
201
|
+
const searchArgs = ["-n", "8"];
|
|
202
|
+
|
|
203
|
+
const sessionArgs = [];
|
|
204
|
+
if (input.transcript_path) {
|
|
205
|
+
sessionArgs.push("--transcript", input.transcript_path);
|
|
206
|
+
}
|
|
207
|
+
if (input.session_id) {
|
|
208
|
+
sessionArgs.push("--session", input.session_id, "--hook-source", "codex");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const args = raw ? [...searchArgs, ...sessionArgs, raw] : ["--recent", ...searchArgs];
|
|
212
|
+
|
|
213
|
+
async function runEi(commandArgs) {
|
|
214
|
+
const direct = await $\`ei \${commandArgs}\`.quiet().text().catch(() => "");
|
|
215
|
+
if (direct.trim()) return direct;
|
|
216
|
+
return await $\`bunx ei-tui@latest \${commandArgs}\`.quiet().text().catch(() => "");
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const output = await runEi(args);
|
|
220
|
+
if (output.trim()) {
|
|
221
|
+
const heading = [
|
|
222
|
+
"## Ei Memory Context",
|
|
223
|
+
"*(The user cannot see this block. It is injected automatically before their message.)*",
|
|
224
|
+
"*(If you reference anything from it, briefly explain where it came from — e.g. \\"Ei shows you've been working on X\\" — so the user isn't confused by knowledge that appeared from nowhere.)*",
|
|
225
|
+
"",
|
|
226
|
+
"Ei is a personal knowledge base built from the user's coding sessions, Slack, documents, and conversations.",
|
|
227
|
+
"The following memories MAY be relevant to your current task — use \`ei_search\` or \`ei_lookup\` for targeted queries.",
|
|
228
|
+
].join("\\n");
|
|
229
|
+
|
|
230
|
+
process.stdout.write(JSON.stringify({
|
|
231
|
+
hookSpecificOutput: {
|
|
232
|
+
hookEventName: "UserPromptSubmit",
|
|
233
|
+
additionalContext: \`\\n\${heading}\\n\${output.trim()}\\n\`,
|
|
234
|
+
},
|
|
235
|
+
}));
|
|
236
|
+
}
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
await Bun.write(scriptPath, scriptContent);
|
|
240
|
+
await Bun.$`chmod +x ${scriptPath}`;
|
|
241
|
+
|
|
242
|
+
type CodexUserPromptHook = {
|
|
243
|
+
hooks: Array<{ type: string; command: string; statusMessage?: string; timeout?: number }>;
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
interface CodexHooksConfig {
|
|
247
|
+
hooks: {
|
|
248
|
+
UserPromptSubmit?: CodexUserPromptHook[];
|
|
249
|
+
[key: string]: unknown;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let hooksConfig: CodexHooksConfig = { hooks: {} };
|
|
254
|
+
try {
|
|
255
|
+
const text = await Bun.file(hooksJsonPath).text();
|
|
256
|
+
hooksConfig = JSON.parse(text) as CodexHooksConfig;
|
|
257
|
+
if (!hooksConfig.hooks || typeof hooksConfig.hooks !== "object") {
|
|
258
|
+
hooksConfig.hooks = {};
|
|
259
|
+
}
|
|
260
|
+
} catch {
|
|
261
|
+
// File doesn't exist or isn't valid JSON — start fresh
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const userPromptSubmit = (hooksConfig.hooks.UserPromptSubmit ?? []) as CodexUserPromptHook[];
|
|
265
|
+
const hookEntry = {
|
|
266
|
+
hooks: [{
|
|
267
|
+
type: "command",
|
|
268
|
+
command: scriptPath,
|
|
269
|
+
statusMessage: "Loading Ei memory context",
|
|
270
|
+
timeout: 30,
|
|
271
|
+
}],
|
|
272
|
+
};
|
|
273
|
+
const alreadyInstalled = userPromptSubmit.some((entry) => hookEntryHasCommand(entry, scriptPath));
|
|
274
|
+
if (!alreadyInstalled) {
|
|
275
|
+
userPromptSubmit.push(hookEntry);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
hooksConfig.hooks.UserPromptSubmit = userPromptSubmit;
|
|
279
|
+
|
|
280
|
+
const tmpPath = `${hooksJsonPath}.ei-install.tmp`;
|
|
281
|
+
await Bun.write(tmpPath, JSON.stringify(hooksConfig, null, 2) + "\n");
|
|
282
|
+
const { rename } = await import(/* @vite-ignore */ "fs/promises");
|
|
283
|
+
await rename(tmpPath, hooksJsonPath);
|
|
284
|
+
|
|
285
|
+
console.log(`✓ Installed Ei Codex context hook to ~/.codex/hooks/ei-inject.ts`);
|
|
286
|
+
console.log(` Use /hooks in Codex to review/trust the hook if prompted.`);
|
|
287
|
+
}
|
|
288
|
+
|
|
120
289
|
async function installClaudeCode(): Promise<void> {
|
|
121
290
|
const home = process.env.HOME || "~";
|
|
122
291
|
const claudeJsonPath = join(home, ".claude.json");
|
|
@@ -217,9 +386,7 @@ if (output.trim()) process.stdout.write(\`\\n\${heading}\\n\${output.trim()}\\n\
|
|
|
217
386
|
const userPromptSubmit = (hooks.UserPromptSubmit ?? []) as unknown[];
|
|
218
387
|
|
|
219
388
|
const hookEntry = { hooks: [{ type: "command", command: "~/.claude/hooks/ei-inject.ts" }] };
|
|
220
|
-
const alreadyInstalled = userPromptSubmit.some(
|
|
221
|
-
(entry) => JSON.stringify(entry) === JSON.stringify(hookEntry)
|
|
222
|
-
);
|
|
389
|
+
const alreadyInstalled = userPromptSubmit.some((entry) => hookEntryHasCommand(entry, "~/.claude/hooks/ei-inject.ts"));
|
|
223
390
|
if (!alreadyInstalled) {
|
|
224
391
|
userPromptSubmit.push(hookEntry);
|
|
225
392
|
}
|
|
@@ -497,6 +664,15 @@ async function getRecentSessionMessages(
|
|
|
497
664
|
if (transcriptPath) {
|
|
498
665
|
try {
|
|
499
666
|
const text = await Bun.file(transcriptPath).text();
|
|
667
|
+
|
|
668
|
+
const { parseCodexRolloutMessages } = await import(
|
|
669
|
+
/* @vite-ignore */ "./integrations/codex/reader.js"
|
|
670
|
+
);
|
|
671
|
+
const codexMessages = parseCodexRolloutMessages(text, sessionId ?? "transcript");
|
|
672
|
+
if (codexMessages.length > 0) {
|
|
673
|
+
return codexMessages.slice(-5).map((m) => `${m.role}: ${m.content}`);
|
|
674
|
+
}
|
|
675
|
+
|
|
500
676
|
const messages: Array<{ content: string }> = [];
|
|
501
677
|
|
|
502
678
|
for (const line of text.split("\n")) {
|
|
@@ -529,7 +705,7 @@ async function getRecentSessionMessages(
|
|
|
529
705
|
}
|
|
530
706
|
}
|
|
531
707
|
|
|
532
|
-
return messages.slice(-
|
|
708
|
+
return messages.slice(-5).map((m) => m.content);
|
|
533
709
|
} catch {
|
|
534
710
|
return [];
|
|
535
711
|
}
|
|
@@ -544,7 +720,7 @@ async function getRecentSessionMessages(
|
|
|
544
720
|
);
|
|
545
721
|
const reader = await createOpenCodeReader();
|
|
546
722
|
const messages = await reader.getMessagesForSession(sessionId);
|
|
547
|
-
return messages.slice(-
|
|
723
|
+
return messages.slice(-5).map((m) => `${m.role}: ${m.content}`);
|
|
548
724
|
}
|
|
549
725
|
|
|
550
726
|
if (hookSource === "cursor") {
|
|
@@ -556,7 +732,20 @@ async function getRecentSessionMessages(
|
|
|
556
732
|
const session =
|
|
557
733
|
sessions.find((s) => s.id === sessionId) ?? sessions[sessions.length - 1];
|
|
558
734
|
if (session) {
|
|
559
|
-
return session.messages.slice(-
|
|
735
|
+
return session.messages.slice(-5).map((m) => `${m.type === 1 ? "user" : "assistant"}: ${m.text}`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (hookSource === "codex") {
|
|
740
|
+
const { CodexReader } = await import(
|
|
741
|
+
/* @vite-ignore */ "./integrations/codex/reader.js"
|
|
742
|
+
);
|
|
743
|
+
const reader = new CodexReader();
|
|
744
|
+
const sessions = await reader.getSessions();
|
|
745
|
+
const session =
|
|
746
|
+
sessions.find((s) => s.id === sessionId) ?? sessions[sessions.length - 1];
|
|
747
|
+
if (session) {
|
|
748
|
+
return session.messages.slice(-5).map((m) => `${m.role}: ${m.content}`);
|
|
560
749
|
}
|
|
561
750
|
}
|
|
562
751
|
} catch {
|
|
@@ -589,6 +778,16 @@ async function main(): Promise<void> {
|
|
|
589
778
|
if (args[0] === "--install") {
|
|
590
779
|
await installMcpClients();
|
|
591
780
|
console.log(`
|
|
781
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
782
|
+
Codex
|
|
783
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
784
|
+
|
|
785
|
+
If Codex was detected, Ei MCP was registered via:
|
|
786
|
+
|
|
787
|
+
codex mcp add ei --env EI_DATA_PATH="${process.env.EI_DATA_PATH ?? "~/.local/share/ei"}" -- bunx ei-tui mcp
|
|
788
|
+
|
|
789
|
+
Restart Codex to activate.
|
|
790
|
+
|
|
592
791
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
593
792
|
OpenCode: add to ~/.config/opencode/opencode.jsonc
|
|
594
793
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -18,7 +18,6 @@ export function filterMessagesForContext(
|
|
|
18
18
|
|
|
19
19
|
return messages.filter((msg) => {
|
|
20
20
|
if (msg.external === true) return false;
|
|
21
|
-
if (msg.context_status === ContextStatusEnum.Always) return true;
|
|
22
21
|
if (msg.context_status === ContextStatusEnum.Never) return false;
|
|
23
22
|
|
|
24
23
|
const msgMs = new Date(msg.timestamp).getTime();
|
|
@@ -475,7 +475,7 @@ export function queueReflectionDrain(personaId: string, state: StateManager): vo
|
|
|
475
475
|
messages_analyze: unextractedPeople,
|
|
476
476
|
extraction_flag: "p",
|
|
477
477
|
};
|
|
478
|
-
queuePersonScan(context, state);
|
|
478
|
+
queuePersonScan(context, state, { reflection_progress: 1 });
|
|
479
479
|
console.log(`[reflection:drain] Queued Person scan for ${persona.display_name} (${unextractedPeople.length} messages) — clears on completion`);
|
|
480
480
|
}
|
|
481
481
|
|
package/src/core/processor.ts
CHANGED
|
@@ -148,6 +148,7 @@ const DEFAULT_LOOP_INTERVAL_MS = 100;
|
|
|
148
148
|
const DEFAULT_OPENCODE_POLLING_MS = 60000;
|
|
149
149
|
const DEFAULT_CLAUDE_CODE_POLLING_MS = 60000;
|
|
150
150
|
const DEFAULT_CURSOR_POLLING_MS = 60000;
|
|
151
|
+
const DEFAULT_CODEX_POLLING_MS = 60000;
|
|
151
152
|
|
|
152
153
|
let processorInstanceCount = 0;
|
|
153
154
|
|
|
@@ -170,6 +171,8 @@ export class Processor {
|
|
|
170
171
|
private claudeCodeImportInProgress = false;
|
|
171
172
|
private lastCursorSync = 0;
|
|
172
173
|
private cursorImportInProgress = false;
|
|
174
|
+
private lastCodexSync = 0;
|
|
175
|
+
private codexImportInProgress = false;
|
|
173
176
|
private lastSlackSync = 0;
|
|
174
177
|
private slackImportInProgress = false;
|
|
175
178
|
private pendingConflict: StateConflictData | null = null;
|
|
@@ -1200,6 +1203,14 @@ export class Processor {
|
|
|
1200
1203
|
modified = true;
|
|
1201
1204
|
}
|
|
1202
1205
|
|
|
1206
|
+
if (!human.settings.codex) {
|
|
1207
|
+
human.settings.codex = {
|
|
1208
|
+
integration: false,
|
|
1209
|
+
polling_interval_ms: 60000,
|
|
1210
|
+
};
|
|
1211
|
+
modified = true;
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1203
1214
|
if (!human.settings.ceremony) {
|
|
1204
1215
|
human.settings.ceremony = {
|
|
1205
1216
|
time: "09:00",
|
|
@@ -1277,6 +1288,14 @@ export class Processor {
|
|
|
1277
1288
|
console.log(`[Processor ${this.instanceId}] Clearing claudeCodeImportInProgress flag`);
|
|
1278
1289
|
this.claudeCodeImportInProgress = false;
|
|
1279
1290
|
}
|
|
1291
|
+
if (this.cursorImportInProgress) {
|
|
1292
|
+
console.log(`[Processor ${this.instanceId}] Clearing cursorImportInProgress flag`);
|
|
1293
|
+
this.cursorImportInProgress = false;
|
|
1294
|
+
}
|
|
1295
|
+
if (this.codexImportInProgress) {
|
|
1296
|
+
console.log(`[Processor ${this.instanceId}] Clearing codexImportInProgress flag`);
|
|
1297
|
+
this.codexImportInProgress = false;
|
|
1298
|
+
}
|
|
1280
1299
|
if (this.slackImportInProgress) {
|
|
1281
1300
|
console.log(`[Processor ${this.instanceId}] Clearing slackImportInProgress flag`);
|
|
1282
1301
|
this.slackImportInProgress = false;
|
|
@@ -1522,6 +1541,14 @@ const toolNextSteps = new Set([
|
|
|
1522
1541
|
await this.checkAndSyncCursor(human, now);
|
|
1523
1542
|
}
|
|
1524
1543
|
|
|
1544
|
+
if (
|
|
1545
|
+
this.isTUI &&
|
|
1546
|
+
human.settings?.codex?.integration &&
|
|
1547
|
+
this.stateManager.queue_length() === 0
|
|
1548
|
+
) {
|
|
1549
|
+
await this.checkAndSyncCodex(human, now);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1525
1552
|
if (
|
|
1526
1553
|
this.isTUI &&
|
|
1527
1554
|
human.settings?.personaHistory?.integration &&
|
|
@@ -1779,6 +1806,60 @@ const toolNextSteps = new Set([
|
|
|
1779
1806
|
});
|
|
1780
1807
|
}
|
|
1781
1808
|
|
|
1809
|
+
private async checkAndSyncCodex(human: HumanEntity, now: number): Promise<void> {
|
|
1810
|
+
if (this.codexImportInProgress) {
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
const codex = human.settings?.codex;
|
|
1815
|
+
const pollingInterval = codex?.polling_interval_ms ?? DEFAULT_CODEX_POLLING_MS;
|
|
1816
|
+
const lastSync = codex?.last_sync ? new Date(codex.last_sync).getTime() : 0;
|
|
1817
|
+
const timeSinceSync = now - lastSync;
|
|
1818
|
+
|
|
1819
|
+
if (timeSinceSync < pollingInterval && this.lastCodexSync > 0) {
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
this.lastCodexSync = now;
|
|
1824
|
+
const syncTimestamp = new Date().toISOString();
|
|
1825
|
+
const currentHuman = this.stateManager.getHuman();
|
|
1826
|
+
this.stateManager.setHuman({
|
|
1827
|
+
...currentHuman,
|
|
1828
|
+
settings: {
|
|
1829
|
+
...currentHuman.settings,
|
|
1830
|
+
codex: {
|
|
1831
|
+
...codex,
|
|
1832
|
+
last_sync: syncTimestamp,
|
|
1833
|
+
},
|
|
1834
|
+
},
|
|
1835
|
+
});
|
|
1836
|
+
|
|
1837
|
+
this.codexImportInProgress = true;
|
|
1838
|
+
import("../integrations/codex/importer.js")
|
|
1839
|
+
.then(({ importCodexSessions }) =>
|
|
1840
|
+
importCodexSessions({
|
|
1841
|
+
stateManager: this.stateManager,
|
|
1842
|
+
interface: this.interface,
|
|
1843
|
+
signal: this.importAbortController.signal,
|
|
1844
|
+
})
|
|
1845
|
+
)
|
|
1846
|
+
.then((result) => {
|
|
1847
|
+
if (result.sessionsProcessed > 0) {
|
|
1848
|
+
console.log(
|
|
1849
|
+
`[Processor] Codex sync complete: ${result.sessionsProcessed} sessions, ` +
|
|
1850
|
+
`${result.messagesImported} messages imported, ` +
|
|
1851
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
})
|
|
1855
|
+
.catch((err) => {
|
|
1856
|
+
console.warn(`[Processor] Codex sync failed:`, err);
|
|
1857
|
+
})
|
|
1858
|
+
.finally(() => {
|
|
1859
|
+
this.codexImportInProgress = false;
|
|
1860
|
+
});
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1782
1863
|
private async checkAndSyncSlack(human: HumanEntity, now: number): Promise<void> {
|
|
1783
1864
|
if (this.slackImportInProgress) return;
|
|
1784
1865
|
|
|
@@ -15,7 +15,7 @@ export interface DataItemBase {
|
|
|
15
15
|
learned_by?: string; // Persona ID that originally learned this item (stable UUID)
|
|
16
16
|
last_changed_by?: string; // Persona ID that most recently updated this item (stable UUID)
|
|
17
17
|
interested_personas?: string[]; // Persona IDs that have extracted/touched this item (accumulated)
|
|
18
|
-
sources?: string[]; // Namespaced source identifiers — where items were learned from. Format: "provider:id" (e.g., "opencode:ses_abc123", "cursor:composerId"). Grow-only union.
|
|
18
|
+
sources?: string[]; // Namespaced source identifiers — where items were learned from. Format: "provider:id" (e.g., "opencode:ses_abc123", "cursor:composerId", "codex:threadId"). Grow-only union.
|
|
19
19
|
persona_groups?: string[];
|
|
20
20
|
embedding?: number[];
|
|
21
21
|
rewrite_length_floor?: number; // Set after every rewrite scan: ceil(description.length * 1.1). Item is skipped by ceremony until description grows past this floor. Preserved across extraction upserts — only cleared when description exceeds it.
|
|
@@ -130,6 +130,7 @@ export interface HumanSettings {
|
|
|
130
130
|
backup?: BackupConfig;
|
|
131
131
|
claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
|
|
132
132
|
cursor?: import("../../integrations/cursor/types.js").CursorSettings;
|
|
133
|
+
codex?: import("../../integrations/codex/types.js").CodexSettings;
|
|
133
134
|
document?: DocumentSettings;
|
|
134
135
|
active_theme?: string;
|
|
135
136
|
custom_themes?: ThemeDefinition[];
|
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
|
|