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/src/cli.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * EI CLI - Memory retrieval interface for OpenCode integration
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/Claude Desktop)
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", "opencode:my-machine", "opencode:my-machine:ses_abc123")
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 "cursor"
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 topics MAY be relevant to your current task — use \\\`ei_search\\\` or \\\`ei_lookup\\\` for targeted queries.
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 ? [...typeArgs, ...sessionArgs, raw] : ["--recent", ...typeArgs];
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(-4).map((m) => m.content);
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(-4).map((m) => m.content);
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(-4).map((m) => m.text);
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
 
@@ -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[];