ei-tui 0.4.2 → 0.4.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": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "author": "Flare576",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli/README.md CHANGED
@@ -10,6 +10,8 @@ ei people -n 5 "query string" # Return up to 5 people
10
10
  ei topics -n 5 "query string" # Return up to 5 topics
11
11
  ei quotes -n 5 "query string" # Return up to 5 quotes
12
12
  ei --persona "Beta" "query string" # Filter results to what Beta has learned
13
+ ei --recent # Most recently mentioned items (no query needed)
14
+ ei --persona "Beta" --recent # Most recently mentioned items Beta has learned
13
15
  ei --id <id> # Look up a specific entity by ID
14
16
  echo <id> | ei --id # Look up entity by ID from stdin
15
17
  ei --install # Register Ei with OpenCode, Claude Code, and Cursor
@@ -110,8 +112,7 @@ conversations (facts, people, topics, quotes).
110
112
  than only code.
111
113
 
112
114
  **How to use:**
113
- 1. Call `ei_search` (server `user-ei`) with a natural-language query; optionally filter by
114
- `type` (facts, people, topics, quotes) or `persona` display_name.
115
+ 1. Call `ei_search` (server `user-ei`) with a natural-language query (or omit query and use `recent: true` to browse); optionally filter by `type` (facts, people, topics, quotes) or `persona` display_name.
115
116
  2. If you need full detail for a result, call `ei_lookup` with the entity `id` from step 1.
116
117
 
117
118
  Prefer querying Ei before asking the user for context they may have already shared.
@@ -123,11 +124,12 @@ The installed tool gives OpenCode agents access to all four data types with prop
123
124
 
124
125
  | Arg | Type | Description |
125
126
  |-----|------|-------------|
126
- | `query` | string (required) | Search text, or entity ID when `lookup=true` |
127
+ | `query` | string (optional) | Search text, or entity ID when `lookup=true`. Omit to browse by recency. |
127
128
  | `persona` | string (optional) | Persona display_name to filter results — only returns entities that persona has extracted |
128
129
  | `type` | enum (optional) | `facts` \| `people` \| `topics` \| `quotes` — omit for balanced results |
129
130
  | `limit` | number (optional) | Max results, default 10 |
130
131
  | `lookup` | boolean (optional) | If true, fetch single entity by ID |
132
+ | `recent` | boolean (optional) | If true, sort by most recently mentioned. Can be combined with `persona` or `query`. |
131
133
 
132
134
  ## Output Shapes
133
135
 
package/src/cli/mcp.ts CHANGED
@@ -16,9 +16,9 @@ export function createMcpServer(): McpServer {
16
16
  "ei_search",
17
17
  {
18
18
  description:
19
- "Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, and quotes. Results include entity IDs that can be passed back to ei_lookup for full detail.",
19
+ "Search the user's Ei knowledge base — a persistent memory store built from conversations. Returns facts, people, topics of interest, and quotes. Results include entity IDs that can be passed back to ei_lookup for full detail. Omit query to browse by recency (use with recent=true or persona filter).",
20
20
  inputSchema: {
21
- query: z.string().describe("Search text. Supports natural language."),
21
+ query: z.string().optional().describe("Search text. Supports natural language. Omit to browse without semantic filtering — useful with recent=true or persona filter."),
22
22
  type: z
23
23
  .enum(["facts", "people", "topics", "quotes"])
24
24
  .optional()
@@ -42,7 +42,8 @@ export function createMcpServer(): McpServer {
42
42
  .describe("If true, sort by most recently mentioned."),
43
43
  },
44
44
  },
45
- async ({ query, type, persona, limit, recent }) => {
45
+ async ({ query: rawQuery, type, persona, limit, recent }) => {
46
+ const query = rawQuery ?? "";
46
47
  const options = { recent: recent ?? false };
47
48
  const effectiveLimit = limit ?? 10;
48
49
 
package/src/cli.ts CHANGED
@@ -84,8 +84,8 @@ function buildOpenCodeToolContent(): string {
84
84
  ' "Results include entity IDs that can be passed back with lookup=true to get full detail.",',
85
85
  ' ].join(" "),',
86
86
  ' args: {',
87
- ' query: tool.schema.string().describe(',
88
- ' "Search text, or an entity ID when lookup=true. Supports natural language."',
87
+ ' query: tool.schema.string().optional().describe(',
88
+ ' "Search text, or an entity ID when lookup=true. Supports natural language. Omit to browse by recency."',
89
89
  ' ),',
90
90
  ' type: tool.schema',
91
91
  ' .enum(["facts", "people", "topics", "quotes"])',
@@ -112,16 +112,23 @@ function buildOpenCodeToolContent(): string {
112
112
  ' .describe(',
113
113
  ' "If true, treat query as an entity ID and return that single entity in full detail."',
114
114
  ' ),',
115
+ ' recent: tool.schema',
116
+ ' .boolean()',
117
+ ' .optional()',
118
+ ' .describe(',
119
+ ' "If true, sort by most recently mentioned. Can be combined with persona or query."',
120
+ ' ),',
115
121
  ' },',
116
122
  ' async execute(args) {',
117
123
  ' const cmd: string[] = ["ei"];',
118
124
  ' if (args.lookup) {',
119
- ' cmd.push("--id", args.query);',
125
+ ' cmd.push("--id", args.query ?? "");',
120
126
  ' } else {',
121
127
  ' if (args.type) cmd.push(args.type);',
122
128
  ' if (args.persona) cmd.push("--persona", args.persona);',
129
+ ' if (args.recent) cmd.push("--recent");',
123
130
  ' if (args.limit && args.limit !== 10) cmd.push("-n", String(args.limit));',
124
- ' cmd.push(args.query);',
131
+ ' if (args.query) cmd.push(args.query);',
125
132
  ' }',
126
133
  ' return Bun.$`${cmd}`.text();',
127
134
  ' },',
@@ -312,18 +319,10 @@ async function main(): Promise<void> {
312
319
 
313
320
  const query = parsed.positionals.join(" ").trim();
314
321
  const limit = parsed.values.number ? parseInt(parsed.values.number, 10) : 10;
315
- const recent = parsed.values.recent === true;
322
+ // Default to recent mode when no query — allows `ei --persona Foo` and `ei` with no args
323
+ const recent = parsed.values.recent === true || !query;
316
324
  const personaName = parsed.values.persona?.trim();
317
325
 
318
- if (!query && !recent) {
319
- if (targetType) {
320
- console.error(`Search text required. Usage: ei ${targetType} "search text"`);
321
- } else {
322
- console.error(`Search text required. Usage: ei "search text"`);
323
- }
324
- process.exit(1);
325
- }
326
-
327
326
  if (isNaN(limit) || limit < 1) {
328
327
  console.error("--number must be a positive integer");
329
328
  process.exit(1);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared timestamp formatter for all LLM-facing date/time strings.
3
+ * Consistent format across system prompt, quotes, and message history
4
+ * so models can trivially compute time deltas.
5
+ */
6
+ const TIMESTAMP_FORMAT: Intl.DateTimeFormatOptions = {
7
+ weekday: 'short',
8
+ year: 'numeric',
9
+ month: 'short',
10
+ day: 'numeric',
11
+ hour: '2-digit',
12
+ minute: '2-digit',
13
+ hour12: false,
14
+ timeZoneName: 'short',
15
+ };
16
+
17
+ export function formatTimestamp(isoString: string): string {
18
+ return new Date(isoString).toLocaleString('en-US', TIMESTAMP_FORMAT);
19
+ }
20
+
21
+ export function formatCurrentTime(): string {
22
+ return formatTimestamp(new Date().toISOString());
23
+ }
@@ -7,6 +7,7 @@ import {
7
7
  type ContextStatus,
8
8
  type LLMRequest,
9
9
  } from "./types.js";
10
+ import { formatTimestamp } from "./format-utils.js";
10
11
  import { StateManager } from "./state-manager.js";
11
12
  import { QueueProcessor } from "./queue-processor.js";
12
13
  import {
@@ -282,9 +283,10 @@ export function fetchMessagesForLLM(
282
283
  return filteredHistory.reduce<import("./types.js").ChatMessage[]>((acc, m) => {
283
284
  const content = buildChatMessageContent(m);
284
285
  if (content.length > 0) {
286
+ const finalContent = persona.include_message_timestamps ? `[${formatTimestamp(m.timestamp)}] ${content}` : content;
285
287
  acc.push({
286
288
  role: m.role === "human" ? "user" : "assistant",
287
- content,
289
+ content: finalContent,
288
290
  });
289
291
  }
290
292
  return acc;
@@ -123,6 +123,7 @@ export interface PersonaEntity {
123
123
  is_static: boolean;
124
124
  heartbeat_delay_ms?: number;
125
125
  context_window_hours?: number;
126
+ include_message_timestamps?: boolean; // Prepend ISO timestamp to each message sent to the LLM
126
127
  context_boundary?: string; // ISO timestamp - messages before this excluded from LLM context
127
128
  last_updated: string;
128
129
  last_activity: string;
@@ -8,6 +8,7 @@
8
8
  */
9
9
 
10
10
  import type { ResponsePromptData, PromptOutput } from "./types.js";
11
+ import { formatCurrentTime } from "../../core/format-utils.js";
11
12
  import {
12
13
  buildIdentitySection,
13
14
  buildGuidelinesSection,
@@ -51,11 +52,7 @@ Your role is unique among personas:
51
52
  const priorities = buildPrioritiesSection(data.persona, data.human);
52
53
  const responseFormat = buildResponseFormatSection();
53
54
  const toolsSection = (data.tools && data.tools.length > 0) ? buildToolsSection() : "";
54
- const currentTime = new Date().toLocaleString('en-US', {
55
- weekday: 'long', year: 'numeric', month: 'long',
56
- day: 'numeric', hour: 'numeric', minute: '2-digit',
57
- timeZoneName: 'short',
58
- });
55
+ const currentTime = formatCurrentTime();
59
56
  const conversationState = getConversationStateText(data.delay_ms);
60
57
 
61
58
  return `${identity}
@@ -100,11 +97,7 @@ function buildStandardSystemPrompt(data: ResponsePromptData): string {
100
97
  const priorities = buildPrioritiesSection(data.persona, data.human);
101
98
  const responseFormat = buildResponseFormatSection();
102
99
  const toolsSection = (data.tools && data.tools.length > 0) ? buildToolsSection() : "";
103
- const currentTime = new Date().toLocaleString('en-US', {
104
- weekday: 'long', year: 'numeric', month: 'long',
105
- day: 'numeric', hour: 'numeric', minute: '2-digit',
106
- timeZoneName: 'short',
107
- });
100
+ const currentTime = formatCurrentTime();
108
101
  const conversationState = getConversationStateText(data.delay_ms);
109
102
 
110
103
  return `${identity}
@@ -5,6 +5,7 @@
5
5
 
6
6
  import type { PersonaTrait, Quote, PersonaTopic } from "../../core/types.js";
7
7
  import type { ResponsePromptData } from "./types.js";
8
+ import { formatTimestamp } from "../../core/format-utils.js";
8
9
 
9
10
  const DESCRIPTION_MAX_CHARS = 500;
10
11
 
@@ -264,8 +265,7 @@ export function getConversationStateText(delayMs: number): string {
264
265
  // =============================================================================
265
266
 
266
267
  function formatDate(isoString: string): string {
267
- const date = new Date(isoString);
268
- return date.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
268
+ return formatTimestamp(isoString);
269
269
  }
270
270
 
271
271
  export function buildQuotesSection(quotes: Quote[], human: ResponsePromptData["human"]): string {
@@ -57,6 +57,7 @@ interface EditablePersonaData {
57
57
  is_paused?: boolean;
58
58
  pause_until?: string;
59
59
  is_static?: boolean;
60
+ include_message_timestamps?: boolean;
60
61
  tools?: Record<string, Record<string, boolean>>;
61
62
  }
62
63
 
@@ -288,6 +289,7 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allT
288
289
  is_paused: persona.is_paused || undefined,
289
290
  pause_until: persona.pause_until,
290
291
  is_static: persona.is_static || undefined,
292
+ include_message_timestamps: persona.include_message_timestamps || undefined,
291
293
  tools: toolsMap,
292
294
  };
293
295
 
@@ -391,6 +393,7 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity, al
391
393
  is_paused: data.is_paused ?? false,
392
394
  pause_until: data.pause_until,
393
395
  is_static: data.is_static ?? false,
396
+ include_message_timestamps: data.include_message_timestamps ?? false,
394
397
  tools: resolvePersonaToolsFromMap(data.tools, allTools ?? [], allProviders ?? []),
395
398
  last_updated: new Date().toISOString(),
396
399
  };