ei-tui 1.6.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ei-tui",
3
- "version": "1.6.2",
3
+ "version": "1.6.3",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli/README.md CHANGED
@@ -36,6 +36,7 @@ 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
38
  ei --id "codex:my-machine:thread-uuid:evt_42"
39
+ ei --id "pi:my-machine:session-uuid:session-uuid/entry-id"
39
40
  ```
40
41
 
41
42
  Quotes surfaced by `ei_search` include a `message_id` field in this format — pipe it to `ei --id` to read the original conversation.
@@ -56,6 +57,7 @@ This registers Ei with Claude Code, Cursor, Codex, and OpenCode — MCP server c
56
57
  | **Cursor** | `~/.cursor/mcp.json` | `~/.cursor/hooks.json` (`beforeSubmitPrompt`) + `~/.cursor/hooks/ei-inject.sh` | — |
57
58
  | **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 |
58
59
  | **OpenCode** | manual (see below) | Via Oh My OpenCode compatibility layer (reads `~/.claude/settings.json`) | `~/.config/opencode/plugins/ei-persona.ts` |
60
+ | **Pi / OMP** | — (tools registered as native Pi extension) | `~/.pi/agent/extensions/ei-integration.ts` (Pi) or `~/.omp/agent/extensions/ei-integration.ts` (OMP) | — |
59
61
 
60
62
  **Context hook**: fires before every message, searches Ei for relevant memory, and injects it silently. No tool call required.
61
63
 
@@ -521,6 +521,28 @@ export async function resolveExternalMessage(
521
521
  }
522
522
  }
523
523
 
524
+ case "pi": {
525
+ if (parsed.machine !== getMachineId()) {
526
+ return { error: `Message is from machine '${parsed.machine}', not available on this machine (${getMachineId()})` };
527
+ }
528
+ try {
529
+ const { PiReader } = await import("../integrations/pi/reader.js");
530
+ const reader = new PiReader();
531
+ const win = await reader.getMessageById(parsed.session!, parsed.nativeId, before, after);
532
+ if (!win) return null;
533
+ return {
534
+ type: "opencode_message",
535
+ message: { id: win.message.id, role: win.message.role, content: win.message.content, timestamp: win.message.timestamp },
536
+ before: win.before.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp })),
537
+ after: win.after.map(m => ({ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp })),
538
+ session: { id: win.session.id, title: win.session.title, directory: win.session.cwd },
539
+ source: "pi",
540
+ };
541
+ } catch {
542
+ return null;
543
+ }
544
+ }
545
+
524
546
  case "unknown":
525
547
  default: {
526
548
  // Backward compat: bare msg_xxx → treat as opencode (no machine qualifier)
package/src/cli.ts CHANGED
@@ -121,6 +121,17 @@ async function installMcpClients(): Promise<void> {
121
121
  } else {
122
122
  console.log(`ℹ️ OpenCode not detected — skipping OpenCode plugin install.`);
123
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
+ }
124
135
  }
125
136
 
126
137
  async function commandExists(command: string): Promise<boolean> {
@@ -351,12 +362,11 @@ const heading = \`
351
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.)*
352
363
 
353
364
  Ei is a personal knowledge base built from the user's coding sessions, Slack, documents, and conversations.
354
- 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.
355
366
  \`;
356
367
 
357
368
  const input = await new Response(Bun.stdin.stream()).json().catch(() => ({}));
358
369
  const raw = (input.prompt ?? "").replace(/<[^>]*>/g, "").trim();
359
- const typeArgs = ["topics", "-n", "5"];
360
370
 
361
371
  const sessionArgs = [];
362
372
  if (input.session_id && input.hook_source) {
@@ -365,7 +375,7 @@ if (input.session_id && input.hook_source) {
365
375
  sessionArgs.push("--transcript", input.transcript_path);
366
376
  }
367
377
 
368
- const args = raw ? [...typeArgs, ...sessionArgs, raw] : ["--recent", ...typeArgs];
378
+ const args = raw ? ["-n", "5", ...sessionArgs, raw] : ["--recent", "-n", "5"];
369
379
 
370
380
  const output = await $\`bunx ei-tui@latest \${args}\`.quiet().text().catch(() => "");
371
381
  if (output.trim()) process.stdout.write(\`\\n\${heading}\\n\${output.trim()}\\n\`);
@@ -507,6 +517,135 @@ exit 0
507
517
  console.log(`✓ Installed Ei context hook to ~/.cursor/hooks/ei-inject.sh`);
508
518
  }
509
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
+
510
649
  async function installOpenCodePlugin(): Promise<void> {
511
650
  const home = process.env.HOME || "~";
512
651
  const opencodeDir = join(home, ".config", "opencode");
@@ -149,6 +149,7 @@ const DEFAULT_OPENCODE_POLLING_MS = 60000;
149
149
  const DEFAULT_CLAUDE_CODE_POLLING_MS = 60000;
150
150
  const DEFAULT_CURSOR_POLLING_MS = 60000;
151
151
  const DEFAULT_CODEX_POLLING_MS = 60000;
152
+ const DEFAULT_PI_POLLING_MS = 60000;
152
153
 
153
154
  let processorInstanceCount = 0;
154
155
 
@@ -173,6 +174,8 @@ export class Processor {
173
174
  private cursorImportInProgress = false;
174
175
  private lastCodexSync = 0;
175
176
  private codexImportInProgress = false;
177
+ private lastPiSync = 0;
178
+ private piImportInProgress = false;
176
179
  private lastSlackSync = 0;
177
180
  private slackImportInProgress = false;
178
181
  private pendingConflict: StateConflictData | null = null;
@@ -1296,6 +1299,10 @@ export class Processor {
1296
1299
  console.log(`[Processor ${this.instanceId}] Clearing codexImportInProgress flag`);
1297
1300
  this.codexImportInProgress = false;
1298
1301
  }
1302
+ if (this.piImportInProgress) {
1303
+ console.log(`[Processor ${this.instanceId}] Clearing piImportInProgress flag`);
1304
+ this.piImportInProgress = false;
1305
+ }
1299
1306
  if (this.slackImportInProgress) {
1300
1307
  console.log(`[Processor ${this.instanceId}] Clearing slackImportInProgress flag`);
1301
1308
  this.slackImportInProgress = false;
@@ -1549,6 +1556,14 @@ const toolNextSteps = new Set([
1549
1556
  await this.checkAndSyncCodex(human, now);
1550
1557
  }
1551
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
+
1552
1567
  if (
1553
1568
  this.isTUI &&
1554
1569
  human.settings?.personaHistory?.integration &&
@@ -1860,6 +1875,60 @@ const toolNextSteps = new Set([
1860
1875
  });
1861
1876
  }
1862
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
+
1863
1932
  private async checkAndSyncSlack(human: HumanEntity, now: number): Promise<void> {
1864
1933
  if (this.slackImportInProgress) return;
1865
1934
 
@@ -131,6 +131,7 @@ export interface HumanSettings {
131
131
  claudeCode?: import("../../integrations/claude-code/types.js").ClaudeCodeSettings;
132
132
  cursor?: import("../../integrations/cursor/types.js").CursorSettings;
133
133
  codex?: import("../../integrations/codex/types.js").CodexSettings;
134
+ pi?: import("../../integrations/pi/types.js").PiSettings;
134
135
  document?: DocumentSettings;
135
136
  active_theme?: string;
136
137
  custom_themes?: ThemeDefinition[];
@@ -15,6 +15,7 @@ export type MessageIdIntegration =
15
15
  | "claudecode"
16
16
  | "cursor"
17
17
  | "codex"
18
+ | "pi"
18
19
  | "import"
19
20
  | "slack"
20
21
  | "unknown"
@@ -79,6 +80,16 @@ export function parseMessageId(id: string): ParsedMessageId {
79
80
  }
80
81
  }
81
82
 
83
+ if (parts[0] === "pi" && parts.length >= 4) {
84
+ return {
85
+ integration: "pi",
86
+ machine: parts[1],
87
+ session: parts[2],
88
+ nativeId: parts.slice(3).join(":"),
89
+ raw: id,
90
+ }
91
+ }
92
+
82
93
  if (parts[0] === "import" && parts[1] === "document" && parts.length >= 4) {
83
94
  return {
84
95
  integration: "import",
@@ -125,6 +136,10 @@ export function qualifyCodexMessage(machine: string, sessionId: string, nativeId
125
136
  return `codex:${machine}:${sessionId}:${nativeId}`
126
137
  }
127
138
 
139
+ export function qualifyPiMessage(machine: string, sessionId: string, nativeId: string): string {
140
+ return `pi:${machine}:${sessionId}:${nativeId}`
141
+ }
142
+
128
143
  export function qualifyDocumentMessage(slug: string, uuid: string): string {
129
144
  return `import:document:${slug}:${uuid}`
130
145
  }
@@ -1,11 +1,9 @@
1
1
  import type { StateManager } from "../../core/state-manager.js";
2
- import type { Ei_Interface, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
2
+ import type { Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
3
3
  import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
4
- import type { IClaudeCodeReader, ClaudeCodeSession, ClaudeCodeMessage } from "./types.js";
5
- import {
6
- CLAUDE_CODE_PERSONA_NAME,
7
- MIN_SESSION_AGE_MS,
8
- } from "./types.js";
4
+ import type { IClaudeCodeReader, ClaudeCodeSession } from "./types.js";
5
+ import { CLAUDE_CODE_PERSONA_NAME } from "./types.js";
6
+ import { MIN_SESSION_AGE_MS } from "../constants.js";
9
7
  import { ClaudeCodeReader } from "./reader.js";
10
8
  import {
11
9
  queueAllScans,
@@ -16,8 +14,10 @@ import {
16
14
  queueTopicRewritePhase,
17
15
  } from "../../core/orchestrators/ceremony.js";
18
16
  import { isProcessRunning } from "../process-check.js";
19
- import { getMachineId } from "../machine-id.js";
20
17
  import { qualifyClaudeCodeMessage } from "../../core/utils/message-id.js";
18
+ import { getMachineId } from "../machine-id.js";
19
+ import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
20
+ import { TWELVE_HOURS_MS } from "../constants.js";
21
21
 
22
22
  // =============================================================================
23
23
  // Export Types
@@ -41,30 +41,9 @@ export interface ClaudeCodeImporterOptions {
41
41
  // Utility Functions
42
42
  // =============================================================================
43
43
 
44
- const TWELVE_HOURS_MS = 43_200_000;
45
44
  const CLAUDE_CODE_GROUP = "Claude Code";
46
45
 
47
- function convertToEiMessage(msg: ClaudeCodeMessage, sessionId: string): Message {
48
- return {
49
- id: qualifyClaudeCodeMessage(getMachineId(), sessionId, msg.id),
50
- role: msg.role === "user" ? "human" : "system",
51
- content: msg.content,
52
- timestamp: msg.timestamp,
53
- read: true,
54
- context_status: "default" as ContextStatus,
55
- external: true,
56
- };
57
- }
58
-
59
- function convertToPreMarkedEiMessage(msg: ClaudeCodeMessage, sessionId: string): Message {
60
- return {
61
- ...convertToEiMessage(msg, sessionId),
62
- f: true,
63
- t: true,
64
- p: true,
65
- e: true,
66
- };
67
- }
46
+ const qualify = qualifyClaudeCodeMessage;
68
47
 
69
48
  /**
70
49
  * Ensure the single "Claude Code" persona exists.
@@ -249,7 +228,7 @@ export async function importClaudeCodeSessions(
249
228
  for (const msg of messages) {
250
229
  const msgMs = new Date(msg.timestamp).getTime();
251
230
  const isOld = cutoffMs !== null && msgMs < cutoffMs;
252
- const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id) : convertToEiMessage(msg, targetSession.id);
231
+ const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id, qualify) : convertToEiMessage(msg, targetSession.id, qualify);
253
232
  stateManager.messages_append(persona.id, eiMsg);
254
233
  result.messagesImported++;
255
234
  if (!isOld) toAnalyze.push(eiMsg);
@@ -139,7 +139,7 @@ export const CLAUDE_CODE_TOPIC_GROUPS = ["General", "Coding", "Claude Code"];
139
139
  * Minimum session age before we import it.
140
140
  * Mirrors OpenCode's 20-minute rule — gives the session time to "settle."
141
141
  */
142
- export const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
142
+ export { MIN_SESSION_AGE_MS } from "../constants.js";
143
143
 
144
144
  // ============================================================================
145
145
  // Human Settings Shape (mirrors OpenCodeSettings in core/types.ts)
@@ -1,5 +1,5 @@
1
1
  import type { StateManager } from "../../core/state-manager.js";
2
- import type { ContextStatus, Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
2
+ import type { Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
3
3
  import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
4
4
  import {
5
5
  queueAllScans,
@@ -10,16 +10,16 @@ import {
10
10
  queueTopicRewritePhase,
11
11
  } from "../../core/orchestrators/ceremony.js";
12
12
  import { qualifyCodexMessage } from "../../core/utils/message-id.js";
13
+ import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
13
14
  import { getMachineId } from "../machine-id.js";
14
15
  import { isProcessRunning } from "../process-check.js";
15
16
  import { CodexReader } from "./reader.js";
16
17
  import {
17
18
  CODEX_PERSONA_NAME,
18
- MIN_SESSION_AGE_MS,
19
- type CodexMessage,
20
19
  type CodexSession,
21
20
  type ICodexReader,
22
21
  } from "./types.js";
22
+ import { MIN_SESSION_AGE_MS, TWELVE_HOURS_MS } from "../constants.js";
23
23
 
24
24
  export interface CodexImportResult {
25
25
  sessionsProcessed: number;
@@ -35,30 +35,9 @@ export interface CodexImporterOptions {
35
35
  signal?: AbortSignal;
36
36
  }
37
37
 
38
- const TWELVE_HOURS_MS = 43_200_000;
39
38
  const CODEX_GROUP = "Codex";
40
39
 
41
- function convertToEiMessage(msg: CodexMessage, sessionId: string): Message {
42
- return {
43
- id: qualifyCodexMessage(getMachineId(), sessionId, msg.id),
44
- role: msg.role === "user" ? "human" : "system",
45
- content: msg.content,
46
- timestamp: msg.timestamp,
47
- read: true,
48
- context_status: "default" as ContextStatus,
49
- external: true,
50
- };
51
- }
52
-
53
- function convertToPreMarkedEiMessage(msg: CodexMessage, sessionId: string): Message {
54
- return {
55
- ...convertToEiMessage(msg, sessionId),
56
- f: true,
57
- t: true,
58
- p: true,
59
- e: true,
60
- };
61
- }
40
+ const qualify = qualifyCodexMessage;
62
41
 
63
42
  function ensureCodexPersona(
64
43
  stateManager: StateManager,
@@ -212,8 +191,8 @@ export async function importCodexSessions(
212
191
  const msgMs = new Date(msg.timestamp).getTime();
213
192
  const isOld = cutoffMs !== null && msgMs < cutoffMs;
214
193
  const eiMsg = isOld
215
- ? convertToPreMarkedEiMessage(msg, targetSession.id)
216
- : convertToEiMessage(msg, targetSession.id);
194
+ ? convertToPreMarkedEiMessage(msg, targetSession.id, qualify)
195
+ : convertToEiMessage(msg, targetSession.id, qualify);
217
196
 
218
197
  stateManager.messages_append(persona.id, eiMsg);
219
198
  result.messagesImported++;
@@ -94,7 +94,7 @@ export const CODEX_TOPIC_GROUPS = ["General", "Coding", "Codex"];
94
94
  * Minimum session age before import.
95
95
  * Mirrors Claude Code / Cursor's 20-minute rule so active sessions can settle.
96
96
  */
97
- export const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
97
+ export { MIN_SESSION_AGE_MS } from "../constants.js";
98
98
 
99
99
  // ============================================================================
100
100
  // Human Settings Shape
@@ -0,0 +1,3 @@
1
+ export const MIN_SESSION_AGE_MS = 20 * 60 * 1_000;
2
+
3
+ export const TWELVE_HOURS_MS = 43_200_000;
@@ -1,15 +1,14 @@
1
1
  import type { StateManager } from "../../core/state-manager.js";
2
- import type { Ei_Interface, Message, ContextStatus, PersonaEntity, PersonaTrait } from "../../core/types.js";
2
+ import type { Ei_Interface, Message, PersonaEntity, PersonaTrait } from "../../core/types.js";
3
3
  import { DEFAULT_SEED_TRAITS } from "../../core/constants/seed-traits.js";
4
4
  import type { ICursorReader, CursorSession, CursorMessage } from "./types.js";
5
- import {
6
- CURSOR_PERSONA_NAME,
7
- MIN_SESSION_AGE_MS,
8
- } from "./types.js";
5
+ import { CURSOR_PERSONA_NAME } from "./types.js";
6
+ import { MIN_SESSION_AGE_MS, TWELVE_HOURS_MS } from "../constants.js";
9
7
  import { CursorReader } from "./reader.js";
10
8
  import { isProcessRunning } from "../process-check.js";
11
9
  import { getMachineId } from "../machine-id.js";
12
10
  import { qualifyCursorMessage } from "../../core/utils/message-id.js";
11
+ import { convertToEiMessage, convertToPreMarkedEiMessage } from "../shared/message-converter.js";
13
12
  import {
14
13
  queueAllScans,
15
14
  type ExtractionContext,
@@ -33,29 +32,12 @@ export interface CursorImporterOptions {
33
32
  signal?: AbortSignal;
34
33
  }
35
34
 
36
- const TWELVE_HOURS_MS = 43_200_000;
37
35
  const CURSOR_GROUP = "Cursor";
38
36
 
39
- function convertToEiMessage(msg: CursorMessage, sessionId: string): Message {
40
- return {
41
- id: qualifyCursorMessage(getMachineId(), sessionId, msg.id),
42
- role: msg.type === 1 ? "human" : "system",
43
- content: msg.text,
44
- timestamp: msg.timestamp,
45
- read: true,
46
- context_status: "default" as ContextStatus,
47
- external: true,
48
- };
49
- }
37
+ const qualify = qualifyCursorMessage;
50
38
 
51
- function convertToPreMarkedEiMessage(msg: CursorMessage, sessionId: string): Message {
52
- return {
53
- ...convertToEiMessage(msg, sessionId),
54
- f: true,
55
- t: true,
56
- p: true,
57
- e: true,
58
- };
39
+ function normalizeCursorMessage(msg: CursorMessage) {
40
+ return { id: msg.id, role: (msg.type === 1 ? "user" : "assistant") as "user" | "assistant", content: msg.text, timestamp: msg.timestamp };
59
41
  }
60
42
 
61
43
  function ensureCursorPersona(
@@ -209,7 +191,8 @@ export async function importCursorSessions(
209
191
  for (const msg of messages) {
210
192
  const msgMs = new Date(msg.timestamp).getTime();
211
193
  const isOld = cutoffMs !== null && msgMs < cutoffMs;
212
- const eiMsg = isOld ? convertToPreMarkedEiMessage(msg, targetSession.id) : convertToEiMessage(msg, targetSession.id);
194
+ const normalized = normalizeCursorMessage(msg);
195
+ const eiMsg = isOld ? convertToPreMarkedEiMessage(normalized, targetSession.id, qualify) : convertToEiMessage(normalized, targetSession.id, qualify);
213
196
  stateManager.messages_append(persona.id, eiMsg);
214
197
  result.messagesImported++;
215
198
  if (!isOld) toAnalyze.push(eiMsg);
@@ -116,7 +116,7 @@ export const CURSOR_TOPIC_GROUPS = ["General", "Coding", "Cursor"];
116
116
  * Minimum session age before we import it.
117
117
  * Mirrors ClaudeCode's 20-minute rule — gives the session time to "settle."
118
118
  */
119
- export const MIN_SESSION_AGE_MS = 20 * 60 * 1000;
119
+ export { MIN_SESSION_AGE_MS } from "../constants.js";
120
120
 
121
121
  // ============================================================================
122
122
  // Human Settings Shape