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/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"),
|
|
@@ -115,6 +121,180 @@ async function installMcpClients(): Promise<void> {
|
|
|
115
121
|
} else {
|
|
116
122
|
console.log(`ℹ️ OpenCode not detected — skipping OpenCode plugin install.`);
|
|
117
123
|
}
|
|
124
|
+
|
|
125
|
+
const hasPi = await Bun.file(join(home, ".pi", "agent", "settings.json")).exists() ||
|
|
126
|
+
await Bun.file(join(home, ".pi", "agent", "auth.json")).exists();
|
|
127
|
+
const hasOmp = await Bun.file(join(home, ".omp", "agent", "settings.json")).exists() ||
|
|
128
|
+
await Bun.file(join(home, ".omp", "agent", "auth.json")).exists();
|
|
129
|
+
|
|
130
|
+
if (hasPi || hasOmp) {
|
|
131
|
+
await installPi();
|
|
132
|
+
} else {
|
|
133
|
+
console.log(`ℹ️ Pi/OMP not detected — skipping Pi extension install.`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function commandExists(command: string): Promise<boolean> {
|
|
138
|
+
try {
|
|
139
|
+
const proc = Bun.spawn([command, "--version"], {
|
|
140
|
+
stdout: "ignore",
|
|
141
|
+
stderr: "ignore",
|
|
142
|
+
});
|
|
143
|
+
await proc.exited;
|
|
144
|
+
return proc.exitCode === 0;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function hookEntryHasCommand(entry: unknown, command: string): boolean {
|
|
151
|
+
if (typeof entry !== "object" || entry === null || !("hooks" in entry)) return false;
|
|
152
|
+
const hooks = (entry as { hooks?: unknown }).hooks;
|
|
153
|
+
if (!Array.isArray(hooks)) return false;
|
|
154
|
+
|
|
155
|
+
return hooks.some((hook) => {
|
|
156
|
+
if (typeof hook !== "object" || hook === null) return false;
|
|
157
|
+
const candidate = hook as { type?: unknown; command?: unknown };
|
|
158
|
+
return candidate.type === "command" && candidate.command === command;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function installCodex(): Promise<void> {
|
|
163
|
+
const dataPath = process.env.EI_DATA_PATH ?? join(process.env.HOME || "~", ".local", "share", "ei");
|
|
164
|
+
const proc = Bun.spawn(
|
|
165
|
+
["codex", "mcp", "add", "ei", "--env", `EI_DATA_PATH=${dataPath}`, "--", "bunx", "ei-tui", "mcp"],
|
|
166
|
+
{
|
|
167
|
+
stdout: "pipe",
|
|
168
|
+
stderr: "pipe",
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
173
|
+
new Response(proc.stdout).text(),
|
|
174
|
+
new Response(proc.stderr).text(),
|
|
175
|
+
proc.exited,
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
if (exitCode !== 0) {
|
|
179
|
+
console.warn(`⚠️ Codex MCP install failed.`);
|
|
180
|
+
const detail = (stderr || stdout).trim();
|
|
181
|
+
if (detail) console.warn(` ${detail}`);
|
|
182
|
+
} else {
|
|
183
|
+
console.log(`✓ Installed Ei MCP server to Codex config (~/.codex/config.toml)`);
|
|
184
|
+
console.log(` Restart Codex to activate MCP.`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
await installCodexHooks();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function installCodexHooks(): Promise<void> {
|
|
191
|
+
const home = process.env.HOME || "~";
|
|
192
|
+
const hooksDir = join(home, ".codex", "hooks");
|
|
193
|
+
const scriptPath = join(hooksDir, "ei-inject.ts");
|
|
194
|
+
const hooksJsonPath = join(home, ".codex", "hooks.json");
|
|
195
|
+
|
|
196
|
+
await Bun.$`mkdir -p ${hooksDir}`;
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
await Bun.$`test -w ${hooksDir}`.quiet();
|
|
200
|
+
} catch {
|
|
201
|
+
console.warn(`⚠️ Cannot write to ${hooksDir} (permission denied).`);
|
|
202
|
+
console.warn(` Fix with: sudo chown ${process.env.USER ?? "$(whoami)"} ${hooksDir}`);
|
|
203
|
+
console.warn(` Then re-run: ei --install`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const scriptContent = `#!/usr/bin/env bun
|
|
208
|
+
import { $ } from "bun";
|
|
209
|
+
|
|
210
|
+
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
211
|
+
const raw = (input.prompt ?? "").replace(/<[^>]*>/g, "").trim();
|
|
212
|
+
const searchArgs = ["-n", "8"];
|
|
213
|
+
|
|
214
|
+
const sessionArgs = [];
|
|
215
|
+
if (input.transcript_path) {
|
|
216
|
+
sessionArgs.push("--transcript", input.transcript_path);
|
|
217
|
+
}
|
|
218
|
+
if (input.session_id) {
|
|
219
|
+
sessionArgs.push("--session", input.session_id, "--hook-source", "codex");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const args = raw ? [...searchArgs, ...sessionArgs, raw] : ["--recent", ...searchArgs];
|
|
223
|
+
|
|
224
|
+
async function runEi(commandArgs) {
|
|
225
|
+
const direct = await $\`ei \${commandArgs}\`.quiet().text().catch(() => "");
|
|
226
|
+
if (direct.trim()) return direct;
|
|
227
|
+
return await $\`bunx ei-tui@latest \${commandArgs}\`.quiet().text().catch(() => "");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const output = await runEi(args);
|
|
231
|
+
if (output.trim()) {
|
|
232
|
+
const heading = [
|
|
233
|
+
"## Ei Memory Context",
|
|
234
|
+
"*(The user cannot see this block. It is injected automatically before their message.)*",
|
|
235
|
+
"*(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.)*",
|
|
236
|
+
"",
|
|
237
|
+
"Ei is a personal knowledge base built from the user's coding sessions, Slack, documents, and conversations.",
|
|
238
|
+
"The following memories MAY be relevant to your current task — use \`ei_search\` or \`ei_lookup\` for targeted queries.",
|
|
239
|
+
].join("\\n");
|
|
240
|
+
|
|
241
|
+
process.stdout.write(JSON.stringify({
|
|
242
|
+
hookSpecificOutput: {
|
|
243
|
+
hookEventName: "UserPromptSubmit",
|
|
244
|
+
additionalContext: \`\\n\${heading}\\n\${output.trim()}\\n\`,
|
|
245
|
+
},
|
|
246
|
+
}));
|
|
247
|
+
}
|
|
248
|
+
`;
|
|
249
|
+
|
|
250
|
+
await Bun.write(scriptPath, scriptContent);
|
|
251
|
+
await Bun.$`chmod +x ${scriptPath}`;
|
|
252
|
+
|
|
253
|
+
type CodexUserPromptHook = {
|
|
254
|
+
hooks: Array<{ type: string; command: string; statusMessage?: string; timeout?: number }>;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
interface CodexHooksConfig {
|
|
258
|
+
hooks: {
|
|
259
|
+
UserPromptSubmit?: CodexUserPromptHook[];
|
|
260
|
+
[key: string]: unknown;
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let hooksConfig: CodexHooksConfig = { hooks: {} };
|
|
265
|
+
try {
|
|
266
|
+
const text = await Bun.file(hooksJsonPath).text();
|
|
267
|
+
hooksConfig = JSON.parse(text) as CodexHooksConfig;
|
|
268
|
+
if (!hooksConfig.hooks || typeof hooksConfig.hooks !== "object") {
|
|
269
|
+
hooksConfig.hooks = {};
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
// File doesn't exist or isn't valid JSON — start fresh
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const userPromptSubmit = (hooksConfig.hooks.UserPromptSubmit ?? []) as CodexUserPromptHook[];
|
|
276
|
+
const hookEntry = {
|
|
277
|
+
hooks: [{
|
|
278
|
+
type: "command",
|
|
279
|
+
command: scriptPath,
|
|
280
|
+
statusMessage: "Loading Ei memory context",
|
|
281
|
+
timeout: 30,
|
|
282
|
+
}],
|
|
283
|
+
};
|
|
284
|
+
const alreadyInstalled = userPromptSubmit.some((entry) => hookEntryHasCommand(entry, scriptPath));
|
|
285
|
+
if (!alreadyInstalled) {
|
|
286
|
+
userPromptSubmit.push(hookEntry);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
hooksConfig.hooks.UserPromptSubmit = userPromptSubmit;
|
|
290
|
+
|
|
291
|
+
const tmpPath = `${hooksJsonPath}.ei-install.tmp`;
|
|
292
|
+
await Bun.write(tmpPath, JSON.stringify(hooksConfig, null, 2) + "\n");
|
|
293
|
+
const { rename } = await import(/* @vite-ignore */ "fs/promises");
|
|
294
|
+
await rename(tmpPath, hooksJsonPath);
|
|
295
|
+
|
|
296
|
+
console.log(`✓ Installed Ei Codex context hook to ~/.codex/hooks/ei-inject.ts`);
|
|
297
|
+
console.log(` Use /hooks in Codex to review/trust the hook if prompted.`);
|
|
118
298
|
}
|
|
119
299
|
|
|
120
300
|
async function installClaudeCode(): Promise<void> {
|
|
@@ -182,12 +362,11 @@ const heading = \`
|
|
|
182
362
|
*(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.)*
|
|
183
363
|
|
|
184
364
|
Ei is a personal knowledge base built from the user's coding sessions, Slack, documents, and conversations.
|
|
185
|
-
The following
|
|
365
|
+
The following items MAY be relevant to your current task — use \\\`ei_search\\\` or \\\`ei_lookup\\\` for targeted queries.
|
|
186
366
|
\`;
|
|
187
367
|
|
|
188
368
|
const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
|
|
189
369
|
const raw = (input.prompt ?? "").replace(/<[^>]*>/g, "").trim();
|
|
190
|
-
const typeArgs = ["topics", "-n", "5"];
|
|
191
370
|
|
|
192
371
|
const sessionArgs = [];
|
|
193
372
|
if (input.session_id && input.hook_source) {
|
|
@@ -196,7 +375,7 @@ if (input.session_id && input.hook_source) {
|
|
|
196
375
|
sessionArgs.push("--transcript", input.transcript_path);
|
|
197
376
|
}
|
|
198
377
|
|
|
199
|
-
const args = raw ? [
|
|
378
|
+
const args = raw ? ["-n", "5", ...sessionArgs, raw] : ["--recent", "-n", "5"];
|
|
200
379
|
|
|
201
380
|
const output = await $\`bunx ei-tui@latest \${args}\`.quiet().text().catch(() => "");
|
|
202
381
|
if (output.trim()) process.stdout.write(\`\\n\${heading}\\n\${output.trim()}\\n\`);
|
|
@@ -217,9 +396,7 @@ if (output.trim()) process.stdout.write(\`\\n\${heading}\\n\${output.trim()}\\n\
|
|
|
217
396
|
const userPromptSubmit = (hooks.UserPromptSubmit ?? []) as unknown[];
|
|
218
397
|
|
|
219
398
|
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
|
-
);
|
|
399
|
+
const alreadyInstalled = userPromptSubmit.some((entry) => hookEntryHasCommand(entry, "~/.claude/hooks/ei-inject.ts"));
|
|
223
400
|
if (!alreadyInstalled) {
|
|
224
401
|
userPromptSubmit.push(hookEntry);
|
|
225
402
|
}
|
|
@@ -340,6 +517,135 @@ exit 0
|
|
|
340
517
|
console.log(`✓ Installed Ei context hook to ~/.cursor/hooks/ei-inject.sh`);
|
|
341
518
|
}
|
|
342
519
|
|
|
520
|
+
async function installPi(): Promise<void> {
|
|
521
|
+
const home = process.env.HOME || "~";
|
|
522
|
+
const dataPath = process.env.EI_DATA_PATH ?? join(home, ".local", "share", "ei");
|
|
523
|
+
|
|
524
|
+
const extensionContent = `import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
525
|
+
import { Type } from "typebox";
|
|
526
|
+
import { $ } from "bun";
|
|
527
|
+
|
|
528
|
+
export default function eiIntegration(pi: ExtensionAPI) {
|
|
529
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
530
|
+
const entries = ctx.sessionManager.getEntries();
|
|
531
|
+
const recentMsgs = entries
|
|
532
|
+
.filter((e: any) => e.type === "message" && (e.message?.role === "user" || e.message?.role === "assistant"))
|
|
533
|
+
.slice(-5)
|
|
534
|
+
.map((e: any) => {
|
|
535
|
+
const role = e.message?.role ?? "unknown";
|
|
536
|
+
const text = Array.isArray(e.message?.content)
|
|
537
|
+
? e.message.content.filter((b: any) => b.type === "text").map((b: any) => b.text).join(" ")
|
|
538
|
+
: (e.message?.content ?? "");
|
|
539
|
+
return \`\${role}: \${text.slice(0, 200)}\`;
|
|
540
|
+
})
|
|
541
|
+
.join("\\n");
|
|
542
|
+
|
|
543
|
+
const prompt = event.prompt ?? "";
|
|
544
|
+
const args = prompt
|
|
545
|
+
? ["-n", "5", "--", prompt]
|
|
546
|
+
: ["--recent", "-n", "5"];
|
|
547
|
+
|
|
548
|
+
const output = await $\`bunx ei-tui@latest \${args}\`
|
|
549
|
+
.env({ ...process.env, EI_DATA_PATH: "${dataPath}" })
|
|
550
|
+
.quiet()
|
|
551
|
+
.text()
|
|
552
|
+
.catch(() => "");
|
|
553
|
+
|
|
554
|
+
if (!output.trim()) return undefined;
|
|
555
|
+
|
|
556
|
+
const heading = [
|
|
557
|
+
"## Ei Memory Context",
|
|
558
|
+
"*(The user cannot see this block. It is injected automatically before their message.)*",
|
|
559
|
+
"*(If you reference anything from it, briefly explain where it came from.)*",
|
|
560
|
+
"",
|
|
561
|
+
"Ei is a personal knowledge base built from your coding sessions, Slack, documents, and conversations.",
|
|
562
|
+
"The following items MAY be relevant to your current task — use ei_search or ei_lookup for targeted queries.",
|
|
563
|
+
].join("\\n");
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
message: {
|
|
567
|
+
customType: "ei-context",
|
|
568
|
+
content: \`\${heading}\\n\\n\${output.trim()}\`,
|
|
569
|
+
display: false,
|
|
570
|
+
},
|
|
571
|
+
};
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
pi.registerTool({
|
|
575
|
+
name: "ei_search",
|
|
576
|
+
label: "Search Ei Memory",
|
|
577
|
+
description: "Semantic search of Ei's personal knowledge base — facts, topics, people, quotes across all sources. Use when you need context about the user, their work, or anything Ei has learned.",
|
|
578
|
+
promptSnippet: "Search Ei's personal memory for relevant facts, topics, people, or quotes.",
|
|
579
|
+
parameters: Type.Object({
|
|
580
|
+
query: Type.String({ description: "Natural language search query" }),
|
|
581
|
+
type: Type.Optional(Type.Union([
|
|
582
|
+
Type.Literal("facts"),
|
|
583
|
+
Type.Literal("topics"),
|
|
584
|
+
Type.Literal("people"),
|
|
585
|
+
Type.Literal("quotes"),
|
|
586
|
+
Type.Literal("personas"),
|
|
587
|
+
], { description: "Filter to a specific data type. Omit for balanced results across all types." })),
|
|
588
|
+
}),
|
|
589
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
590
|
+
const args = params.type
|
|
591
|
+
? [params.type, "-n", "5", "--", params.query]
|
|
592
|
+
: ["-n", "5", "--", params.query];
|
|
593
|
+
const output = await $\`bunx ei-tui@latest \${args}\`
|
|
594
|
+
.env({ ...process.env, EI_DATA_PATH: "${dataPath}" })
|
|
595
|
+
.quiet()
|
|
596
|
+
.text()
|
|
597
|
+
.catch(() => "No results found");
|
|
598
|
+
return {
|
|
599
|
+
content: [{ type: "text" as const, text: output.trim() || "No results found" }],
|
|
600
|
+
details: {},
|
|
601
|
+
};
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
pi.registerTool({
|
|
606
|
+
name: "ei_lookup",
|
|
607
|
+
label: "Lookup Ei Entity",
|
|
608
|
+
description: "Full-record lookup for a specific Ei entity (Fact, Topic, Person, Quote, or Persona) by ID. Use after ei_search to retrieve complete details for an item.",
|
|
609
|
+
parameters: Type.Object({
|
|
610
|
+
id: Type.String({ description: "Entity ID from ei_search results" }),
|
|
611
|
+
}),
|
|
612
|
+
async execute(_id, params, _signal, _onUpdate, _ctx) {
|
|
613
|
+
const output = await $\`bunx ei-tui@latest --id \${params.id}\`
|
|
614
|
+
.env({ ...process.env, EI_DATA_PATH: "${dataPath}" })
|
|
615
|
+
.quiet()
|
|
616
|
+
.text()
|
|
617
|
+
.catch(() => "Not found");
|
|
618
|
+
return {
|
|
619
|
+
content: [{ type: "text" as const, text: output.trim() || "Not found" }],
|
|
620
|
+
details: {},
|
|
621
|
+
};
|
|
622
|
+
},
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
`;
|
|
626
|
+
|
|
627
|
+
const piExtDir = join(home, ".pi", "agent", "extensions");
|
|
628
|
+
const ompExtDir = join(home, ".omp", "agent", "extensions");
|
|
629
|
+
const extFilename = "ei-integration.ts";
|
|
630
|
+
|
|
631
|
+
const hasPiAgent = await Bun.file(join(home, ".pi", "agent", "auth.json")).exists() ||
|
|
632
|
+
await Bun.file(join(home, ".pi", "agent", "settings.json")).exists();
|
|
633
|
+
const hasOmpAgent = await Bun.file(join(home, ".omp", "agent", "auth.json")).exists() ||
|
|
634
|
+
await Bun.file(join(home, ".omp", "agent", "settings.json")).exists();
|
|
635
|
+
|
|
636
|
+
if (hasPiAgent) {
|
|
637
|
+
await Bun.$`mkdir -p ${piExtDir}`;
|
|
638
|
+
await Bun.write(join(piExtDir, extFilename), extensionContent);
|
|
639
|
+
console.log(`✓ Installed Ei extension to ~/.pi/agent/extensions/${extFilename}`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
if (hasOmpAgent) {
|
|
643
|
+
await Bun.$`mkdir -p ${ompExtDir}`;
|
|
644
|
+
await Bun.write(join(ompExtDir, extFilename), extensionContent);
|
|
645
|
+
console.log(`✓ Installed Ei extension to ~/.omp/agent/extensions/${extFilename}`);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
343
649
|
async function installOpenCodePlugin(): Promise<void> {
|
|
344
650
|
const home = process.env.HOME || "~";
|
|
345
651
|
const opencodeDir = join(home, ".config", "opencode");
|
|
@@ -497,6 +803,15 @@ async function getRecentSessionMessages(
|
|
|
497
803
|
if (transcriptPath) {
|
|
498
804
|
try {
|
|
499
805
|
const text = await Bun.file(transcriptPath).text();
|
|
806
|
+
|
|
807
|
+
const { parseCodexRolloutMessages } = await import(
|
|
808
|
+
/* @vite-ignore */ "./integrations/codex/reader.js"
|
|
809
|
+
);
|
|
810
|
+
const codexMessages = parseCodexRolloutMessages(text, sessionId ?? "transcript");
|
|
811
|
+
if (codexMessages.length > 0) {
|
|
812
|
+
return codexMessages.slice(-5).map((m) => `${m.role}: ${m.content}`);
|
|
813
|
+
}
|
|
814
|
+
|
|
500
815
|
const messages: Array<{ content: string }> = [];
|
|
501
816
|
|
|
502
817
|
for (const line of text.split("\n")) {
|
|
@@ -529,7 +844,7 @@ async function getRecentSessionMessages(
|
|
|
529
844
|
}
|
|
530
845
|
}
|
|
531
846
|
|
|
532
|
-
return messages.slice(-
|
|
847
|
+
return messages.slice(-5).map((m) => m.content);
|
|
533
848
|
} catch {
|
|
534
849
|
return [];
|
|
535
850
|
}
|
|
@@ -544,7 +859,7 @@ async function getRecentSessionMessages(
|
|
|
544
859
|
);
|
|
545
860
|
const reader = await createOpenCodeReader();
|
|
546
861
|
const messages = await reader.getMessagesForSession(sessionId);
|
|
547
|
-
return messages.slice(-
|
|
862
|
+
return messages.slice(-5).map((m) => `${m.role}: ${m.content}`);
|
|
548
863
|
}
|
|
549
864
|
|
|
550
865
|
if (hookSource === "cursor") {
|
|
@@ -556,7 +871,20 @@ async function getRecentSessionMessages(
|
|
|
556
871
|
const session =
|
|
557
872
|
sessions.find((s) => s.id === sessionId) ?? sessions[sessions.length - 1];
|
|
558
873
|
if (session) {
|
|
559
|
-
return session.messages.slice(-
|
|
874
|
+
return session.messages.slice(-5).map((m) => `${m.type === 1 ? "user" : "assistant"}: ${m.text}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
if (hookSource === "codex") {
|
|
879
|
+
const { CodexReader } = await import(
|
|
880
|
+
/* @vite-ignore */ "./integrations/codex/reader.js"
|
|
881
|
+
);
|
|
882
|
+
const reader = new CodexReader();
|
|
883
|
+
const sessions = await reader.getSessions();
|
|
884
|
+
const session =
|
|
885
|
+
sessions.find((s) => s.id === sessionId) ?? sessions[sessions.length - 1];
|
|
886
|
+
if (session) {
|
|
887
|
+
return session.messages.slice(-5).map((m) => `${m.role}: ${m.content}`);
|
|
560
888
|
}
|
|
561
889
|
}
|
|
562
890
|
} catch {
|
|
@@ -589,6 +917,16 @@ async function main(): Promise<void> {
|
|
|
589
917
|
if (args[0] === "--install") {
|
|
590
918
|
await installMcpClients();
|
|
591
919
|
console.log(`
|
|
920
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
921
|
+
Codex
|
|
922
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
923
|
+
|
|
924
|
+
If Codex was detected, Ei MCP was registered via:
|
|
925
|
+
|
|
926
|
+
codex mcp add ei --env EI_DATA_PATH="${process.env.EI_DATA_PATH ?? "~/.local/share/ei"}" -- bunx ei-tui mcp
|
|
927
|
+
|
|
928
|
+
Restart Codex to activate.
|
|
929
|
+
|
|
592
930
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
593
931
|
OpenCode: add to ~/.config/opencode/opencode.jsonc
|
|
594
932
|
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
@@ -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,8 @@ 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;
|
|
152
|
+
const DEFAULT_PI_POLLING_MS = 60000;
|
|
151
153
|
|
|
152
154
|
let processorInstanceCount = 0;
|
|
153
155
|
|
|
@@ -170,6 +172,10 @@ export class Processor {
|
|
|
170
172
|
private claudeCodeImportInProgress = false;
|
|
171
173
|
private lastCursorSync = 0;
|
|
172
174
|
private cursorImportInProgress = false;
|
|
175
|
+
private lastCodexSync = 0;
|
|
176
|
+
private codexImportInProgress = false;
|
|
177
|
+
private lastPiSync = 0;
|
|
178
|
+
private piImportInProgress = false;
|
|
173
179
|
private lastSlackSync = 0;
|
|
174
180
|
private slackImportInProgress = false;
|
|
175
181
|
private pendingConflict: StateConflictData | null = null;
|
|
@@ -1200,6 +1206,14 @@ export class Processor {
|
|
|
1200
1206
|
modified = true;
|
|
1201
1207
|
}
|
|
1202
1208
|
|
|
1209
|
+
if (!human.settings.codex) {
|
|
1210
|
+
human.settings.codex = {
|
|
1211
|
+
integration: false,
|
|
1212
|
+
polling_interval_ms: 60000,
|
|
1213
|
+
};
|
|
1214
|
+
modified = true;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1203
1217
|
if (!human.settings.ceremony) {
|
|
1204
1218
|
human.settings.ceremony = {
|
|
1205
1219
|
time: "09:00",
|
|
@@ -1277,6 +1291,18 @@ export class Processor {
|
|
|
1277
1291
|
console.log(`[Processor ${this.instanceId}] Clearing claudeCodeImportInProgress flag`);
|
|
1278
1292
|
this.claudeCodeImportInProgress = false;
|
|
1279
1293
|
}
|
|
1294
|
+
if (this.cursorImportInProgress) {
|
|
1295
|
+
console.log(`[Processor ${this.instanceId}] Clearing cursorImportInProgress flag`);
|
|
1296
|
+
this.cursorImportInProgress = false;
|
|
1297
|
+
}
|
|
1298
|
+
if (this.codexImportInProgress) {
|
|
1299
|
+
console.log(`[Processor ${this.instanceId}] Clearing codexImportInProgress flag`);
|
|
1300
|
+
this.codexImportInProgress = false;
|
|
1301
|
+
}
|
|
1302
|
+
if (this.piImportInProgress) {
|
|
1303
|
+
console.log(`[Processor ${this.instanceId}] Clearing piImportInProgress flag`);
|
|
1304
|
+
this.piImportInProgress = false;
|
|
1305
|
+
}
|
|
1280
1306
|
if (this.slackImportInProgress) {
|
|
1281
1307
|
console.log(`[Processor ${this.instanceId}] Clearing slackImportInProgress flag`);
|
|
1282
1308
|
this.slackImportInProgress = false;
|
|
@@ -1522,6 +1548,22 @@ const toolNextSteps = new Set([
|
|
|
1522
1548
|
await this.checkAndSyncCursor(human, now);
|
|
1523
1549
|
}
|
|
1524
1550
|
|
|
1551
|
+
if (
|
|
1552
|
+
this.isTUI &&
|
|
1553
|
+
human.settings?.codex?.integration &&
|
|
1554
|
+
this.stateManager.queue_length() === 0
|
|
1555
|
+
) {
|
|
1556
|
+
await this.checkAndSyncCodex(human, now);
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
if (
|
|
1560
|
+
this.isTUI &&
|
|
1561
|
+
human.settings?.pi?.integration &&
|
|
1562
|
+
this.stateManager.queue_length() === 0
|
|
1563
|
+
) {
|
|
1564
|
+
await this.checkAndSyncPi(human, now);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1525
1567
|
if (
|
|
1526
1568
|
this.isTUI &&
|
|
1527
1569
|
human.settings?.personaHistory?.integration &&
|
|
@@ -1779,6 +1821,114 @@ const toolNextSteps = new Set([
|
|
|
1779
1821
|
});
|
|
1780
1822
|
}
|
|
1781
1823
|
|
|
1824
|
+
private async checkAndSyncCodex(human: HumanEntity, now: number): Promise<void> {
|
|
1825
|
+
if (this.codexImportInProgress) {
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
const codex = human.settings?.codex;
|
|
1830
|
+
const pollingInterval = codex?.polling_interval_ms ?? DEFAULT_CODEX_POLLING_MS;
|
|
1831
|
+
const lastSync = codex?.last_sync ? new Date(codex.last_sync).getTime() : 0;
|
|
1832
|
+
const timeSinceSync = now - lastSync;
|
|
1833
|
+
|
|
1834
|
+
if (timeSinceSync < pollingInterval && this.lastCodexSync > 0) {
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
this.lastCodexSync = now;
|
|
1839
|
+
const syncTimestamp = new Date().toISOString();
|
|
1840
|
+
const currentHuman = this.stateManager.getHuman();
|
|
1841
|
+
this.stateManager.setHuman({
|
|
1842
|
+
...currentHuman,
|
|
1843
|
+
settings: {
|
|
1844
|
+
...currentHuman.settings,
|
|
1845
|
+
codex: {
|
|
1846
|
+
...codex,
|
|
1847
|
+
last_sync: syncTimestamp,
|
|
1848
|
+
},
|
|
1849
|
+
},
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
this.codexImportInProgress = true;
|
|
1853
|
+
import("../integrations/codex/importer.js")
|
|
1854
|
+
.then(({ importCodexSessions }) =>
|
|
1855
|
+
importCodexSessions({
|
|
1856
|
+
stateManager: this.stateManager,
|
|
1857
|
+
interface: this.interface,
|
|
1858
|
+
signal: this.importAbortController.signal,
|
|
1859
|
+
})
|
|
1860
|
+
)
|
|
1861
|
+
.then((result) => {
|
|
1862
|
+
if (result.sessionsProcessed > 0) {
|
|
1863
|
+
console.log(
|
|
1864
|
+
`[Processor] Codex sync complete: ${result.sessionsProcessed} sessions, ` +
|
|
1865
|
+
`${result.messagesImported} messages imported, ` +
|
|
1866
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
})
|
|
1870
|
+
.catch((err) => {
|
|
1871
|
+
console.warn(`[Processor] Codex sync failed:`, err);
|
|
1872
|
+
})
|
|
1873
|
+
.finally(() => {
|
|
1874
|
+
this.codexImportInProgress = false;
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
private async checkAndSyncPi(human: HumanEntity, now: number): Promise<void> {
|
|
1879
|
+
if (this.piImportInProgress) {
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
const pi = human.settings?.pi;
|
|
1884
|
+
const pollingInterval = pi?.polling_interval_ms ?? DEFAULT_PI_POLLING_MS;
|
|
1885
|
+
const lastSync = pi?.last_sync ? new Date(pi.last_sync).getTime() : 0;
|
|
1886
|
+
const timeSinceSync = now - lastSync;
|
|
1887
|
+
|
|
1888
|
+
if (timeSinceSync < pollingInterval && this.lastPiSync > 0) {
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
this.lastPiSync = now;
|
|
1893
|
+
const syncTimestamp = new Date().toISOString();
|
|
1894
|
+
const currentHuman = this.stateManager.getHuman();
|
|
1895
|
+
this.stateManager.setHuman({
|
|
1896
|
+
...currentHuman,
|
|
1897
|
+
settings: {
|
|
1898
|
+
...currentHuman.settings,
|
|
1899
|
+
pi: {
|
|
1900
|
+
...pi,
|
|
1901
|
+
last_sync: syncTimestamp,
|
|
1902
|
+
},
|
|
1903
|
+
},
|
|
1904
|
+
});
|
|
1905
|
+
|
|
1906
|
+
this.piImportInProgress = true;
|
|
1907
|
+
import("../integrations/pi/importer.js")
|
|
1908
|
+
.then(({ importPiSessions }) =>
|
|
1909
|
+
importPiSessions({
|
|
1910
|
+
stateManager: this.stateManager,
|
|
1911
|
+
interface: this.interface,
|
|
1912
|
+
signal: this.importAbortController.signal,
|
|
1913
|
+
})
|
|
1914
|
+
)
|
|
1915
|
+
.then((result) => {
|
|
1916
|
+
if (result.sessionsProcessed > 0) {
|
|
1917
|
+
console.log(
|
|
1918
|
+
`[Processor] Pi sync complete: ${result.sessionsProcessed} sessions, ` +
|
|
1919
|
+
`${result.messagesImported} messages imported, ` +
|
|
1920
|
+
`${result.extractionScansQueued} extraction scans queued`
|
|
1921
|
+
);
|
|
1922
|
+
}
|
|
1923
|
+
})
|
|
1924
|
+
.catch((err) => {
|
|
1925
|
+
console.warn(`[Processor] Pi sync failed:`, err);
|
|
1926
|
+
})
|
|
1927
|
+
.finally(() => {
|
|
1928
|
+
this.piImportInProgress = false;
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1782
1932
|
private async checkAndSyncSlack(human: HumanEntity, now: number): Promise<void> {
|
|
1783
1933
|
if (this.slackImportInProgress) return;
|
|
1784
1934
|
|
|
@@ -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,8 @@ 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;
|
|
134
|
+
pi?: import("../../integrations/pi/types.js").PiSettings;
|
|
133
135
|
document?: DocumentSettings;
|
|
134
136
|
active_theme?: string;
|
|
135
137
|
custom_themes?: ThemeDefinition[];
|