ei-tui 1.6.9 → 1.7.1

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.
Files changed (42) hide show
  1. package/README.md +4 -0
  2. package/package.json +2 -1
  3. package/skills/coding-harness-reflect/SKILL.md +230 -0
  4. package/skills/ei-curate/SKILL.md +174 -0
  5. package/skills/ei-curate/references/cli.md +160 -0
  6. package/skills/ei-curate/references/provenance.md +88 -0
  7. package/skills/ei-curate/references/recipes.md +144 -0
  8. package/skills/ei-curate/references/talking-to-the-user.md +71 -0
  9. package/skills/ei-persona/SKILL.md +238 -0
  10. package/skills/ei-persona/references/cli.md +265 -0
  11. package/skills/ei-persona/references/recipes.md +247 -0
  12. package/skills/ei-persona/references/talking-to-the-user.md +107 -0
  13. package/src/cli/README.md +72 -2
  14. package/src/cli/corrections-endpoints.ts +297 -0
  15. package/src/cli/corrections-writer.ts +138 -0
  16. package/src/cli/install.ts +117 -25
  17. package/src/cli/mcp.ts +80 -1
  18. package/src/cli/persona-corrections.ts +442 -0
  19. package/src/cli/retrieval.ts +46 -2
  20. package/src/cli.ts +131 -1
  21. package/src/core/corrections.ts +233 -0
  22. package/src/core/handlers/human-extraction.ts +64 -15
  23. package/src/core/handlers/human-matching.ts +2 -2
  24. package/src/core/orchestrators/human-extraction.ts +1 -11
  25. package/src/core/persona-manager.ts +18 -7
  26. package/src/core/persona-tools.ts +92 -0
  27. package/src/core/processor.ts +113 -1
  28. package/src/core/state/human.ts +10 -0
  29. package/src/core/state/personas.ts +8 -0
  30. package/src/core/state-manager.ts +11 -0
  31. package/src/core/utils/identifier-utils.ts +3 -2
  32. package/src/prompts/human/person-scan.ts +2 -0
  33. package/src/prompts/human/person-update.ts +14 -3
  34. package/src/storage/file-lock.ts +120 -0
  35. package/tui/README.md +2 -0
  36. package/tui/src/commands/provider.tsx +1 -1
  37. package/tui/src/components/WelcomeOverlay.tsx +3 -3
  38. package/tui/src/context/ei.tsx +14 -0
  39. package/tui/src/index.tsx +13 -1
  40. package/tui/src/storage/file.ts +15 -83
  41. package/tui/src/util/instance-lock.ts +3 -2
  42. package/tui/src/util/yaml-persona.ts +7 -38
@@ -1,12 +1,96 @@
1
1
  import { join } from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { homedir } from "os";
4
+ import { cp, mkdir, readdir, rename, rm, stat } from "fs/promises";
5
+
6
+ /**
7
+ * Copy every skills/<name>/ directory from Ei's own package into a
8
+ * harness's native skill-discovery directory (targetDir). Copy, not
9
+ * symlink — a symlink into an npm/bunx-installed package's cache breaks
10
+ * silently on upgrade/uninstall, and Windows symlinks need elevated
11
+ * permissions; every other install* function in this file already
12
+ * materializes content onto disk rather than referencing back to source.
13
+ * Generic over whatever exists under skills/ — adding a new Ei-shipped
14
+ * skill later requires zero changes here. Overwrites unconditionally on
15
+ * every run, same as the extension files below.
16
+ *
17
+ * `sourceDir` defaults to Ei's own packaged skills/ (resolved relative to
18
+ * this file's own location, so it works regardless of install method —
19
+ * global npm, bunx, or a from-source checkout) and exists as a parameter
20
+ * purely so tests can redirect it at a fixture directory instead.
21
+ */
22
+ export async function installSkillsTo(targetDir: string, sourceDir?: string): Promise<void> {
23
+ // `.pathname` on a file:// URL keeps a leading slash before a Windows
24
+ // drive letter (`/C:/Users/...`), which fs APIs on Windows do not accept
25
+ // as an absolute path — fileURLToPath() normalizes it correctly on every OS.
26
+ const skillsSourceDir = sourceDir ?? fileURLToPath(new URL("../../skills", import.meta.url));
27
+
28
+ // Plain fs.stat instead of shelling out to `test -d` — `test` is not a
29
+ // Bun Shell builtin (it falls back to a PATH lookup), and stock Windows
30
+ // ships no `test` binary, so the old shell-based check silently treated
31
+ // every Windows install as "source doesn't exist" and skipped skill
32
+ // installation with no warning at all.
33
+ try {
34
+ const sourceStat = await stat(skillsSourceDir);
35
+ if (!sourceStat.isDirectory()) return;
36
+ } catch {
37
+ // From-source checkout or a package build that predates this feature —
38
+ // nothing to do yet, and that's not an error.
39
+ return;
40
+ }
41
+
42
+ // fs.readdir + isDirectory() instead of `ls -d dir/*/` — skips stray files
43
+ // directly under skills/ (e.g. a top-level README.md) without depending on
44
+ // Bun Shell's undocumented `-d` flag support or POSIX glob semantics.
45
+ const entries = await readdir(skillsSourceDir, { withFileTypes: true });
46
+ const skillNames = entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name);
47
+ if (skillNames.length === 0) return;
48
+
49
+ await mkdir(targetDir, { recursive: true });
50
+
51
+ for (const skillName of skillNames) {
52
+ const dest = join(targetDir, skillName);
53
+ // fs.cp merges into an existing dest rather than nesting like a naive
54
+ // `cp -r` would, but it won't remove files that only exist in a stale
55
+ // prior copy — clear first so every run leaves an exact mirror of the
56
+ // current source, matching the unconditional full-overwrite behavior
57
+ // every other install* function in this file gets via Bun.write.
58
+ await rm(dest, { recursive: true, force: true });
59
+ await cp(join(skillsSourceDir, skillName), dest, { recursive: true });
60
+ }
61
+
62
+ console.log(`✓ Installed ${skillNames.length} skill(s) to ${targetDir}`);
63
+ }
64
+
65
+ function resolveHome(): string {
66
+ // Plain Windows shells (cmd.exe, PowerShell without Git Bash/WSL) don't set
67
+ // $HOME — os.homedir() reads USERPROFILE there instead. Without this
68
+ // fallback, `home` silently became the literal string "~", and every
69
+ // downstream join("~", ...) resolved relative to CWD instead of the user's
70
+ // actual profile directory.
71
+ return process.env.HOME || homedir();
72
+ }
73
+
74
+ export async function runInstallStep(label: string, step: () => Promise<void>): Promise<boolean> {
75
+ try {
76
+ await step();
77
+ return true;
78
+ } catch (e) {
79
+ console.warn(`⚠️ ${label} install step failed: ${e instanceof Error ? e.message : String(e)}`);
80
+ console.warn(` Skipping — other integrations will still be attempted.`);
81
+ return false;
82
+ }
83
+ }
2
84
 
3
85
  export async function installMcpClients(): Promise<void> {
4
- await installClaudeCode();
86
+ const failures: string[] = [];
87
+
88
+ if (!(await runInstallStep("Claude Code", installClaudeCode))) failures.push("Claude Code");
5
89
 
6
- const home = process.env.HOME || "~";
90
+ const home = resolveHome();
7
91
 
8
92
  if (await commandExists("codex")) {
9
- await installCodex();
93
+ if (!(await runInstallStep("Codex", installCodex))) failures.push("Codex");
10
94
  } else {
11
95
  console.log(`ℹ️ Codex CLI not detected — skipping Codex MCP install.`);
12
96
  }
@@ -18,7 +102,7 @@ export async function installMcpClients(): Promise<void> {
18
102
  ];
19
103
  const hasCursor = (await Promise.all(cursorDataDirs.map((p) => Bun.file(join(p, "User")).exists()))).some(Boolean);
20
104
  if (hasCursor) {
21
- await installCursor();
105
+ if (!(await runInstallStep("Cursor", installCursor))) failures.push("Cursor");
22
106
  } else {
23
107
  console.log(`ℹ️ Cursor not detected — skipping Cursor install.`);
24
108
  }
@@ -29,7 +113,7 @@ export async function installMcpClients(): Promise<void> {
29
113
  await Bun.file(join(opencodeDir, "opencode.db")).exists();
30
114
 
31
115
  if (hasOpenCode) {
32
- await installOpenCodePlugin();
116
+ if (!(await runInstallStep("OpenCode plugin", installOpenCodePlugin))) failures.push("OpenCode plugin");
33
117
  } else {
34
118
  console.log(`ℹ️ OpenCode not detected — skipping OpenCode plugin install.`);
35
119
  }
@@ -39,7 +123,7 @@ export async function installMcpClients(): Promise<void> {
39
123
  await Bun.file(join(home, ".pi", "agent", "auth.json")).exists();
40
124
 
41
125
  if (hasPi) {
42
- await installPi();
126
+ if (!(await runInstallStep("Pi extension", installPi))) failures.push("Pi extension");
43
127
  } else {
44
128
  console.log(`ℹ️ Pi not detected — skipping Pi extension install.`);
45
129
  }
@@ -51,10 +135,16 @@ export async function installMcpClients(): Promise<void> {
51
135
  await Bun.file(join(home, ".omp", "agent", "agent.db")).exists();
52
136
 
53
137
  if (hasOmp) {
54
- await installOmp();
138
+ if (!(await runInstallStep("OMP extension", installOmp))) failures.push("OMP extension");
55
139
  } else {
56
140
  console.log(`ℹ️ OMP not detected — skipping OMP extension install.`);
57
141
  }
142
+
143
+ if (failures.length > 0) {
144
+ throw new Error(
145
+ `${failures.length} integration(s) failed to install: ${failures.join(", ")}. See warnings above for details.`,
146
+ );
147
+ }
58
148
  }
59
149
 
60
150
  async function commandExists(command: string): Promise<boolean> {
@@ -83,7 +173,7 @@ function hookEntryHasCommand(entry: unknown, command: string): boolean {
83
173
  }
84
174
 
85
175
  async function installCodex(): Promise<void> {
86
- const dataPath = process.env.EI_DATA_PATH ?? join(process.env.HOME || "~", ".local", "share", "ei");
176
+ const dataPath = process.env.EI_DATA_PATH ?? join(resolveHome(), ".local", "share", "ei");
87
177
  const proc = Bun.spawn(
88
178
  ["codex", "mcp", "add", "ei", "--env", `EI_DATA_PATH=${dataPath}`, "--", "bunx", "ei-tui", "mcp"],
89
179
  {
@@ -111,7 +201,7 @@ async function installCodex(): Promise<void> {
111
201
  }
112
202
 
113
203
  async function installCodexHooks(): Promise<void> {
114
- const home = process.env.HOME || "~";
204
+ const home = resolveHome();
115
205
  const hooksDir = join(home, ".codex", "hooks");
116
206
  const scriptPath = join(hooksDir, "ei-inject.ts");
117
207
  const hooksJsonPath = join(home, ".codex", "hooks.json");
@@ -215,15 +305,14 @@ if (import.meta.main) {
215
305
 
216
306
  const tmpPath = `${hooksJsonPath}.ei-install.tmp`;
217
307
  await Bun.write(tmpPath, JSON.stringify(hooksConfig, null, 2) + "\n");
218
- const { rename } = await import(/* @vite-ignore */ "fs/promises");
219
308
  await rename(tmpPath, hooksJsonPath);
220
309
 
221
310
  console.log(`✓ Installed Ei Codex context hook to ~/.codex/hooks/ei-inject.ts`);
222
311
  console.log(` Use /hooks in Codex to review/trust the hook if prompted.`);
223
312
  }
224
313
 
225
- async function installClaudeCode(): Promise<void> {
226
- const home = process.env.HOME || "~";
314
+ export async function installClaudeCode(): Promise<void> {
315
+ const home = resolveHome();
227
316
  const claudeJsonPath = join(home, ".claude.json");
228
317
 
229
318
  // Claude Code supports ${VAR} substitution in env values, resolved from its
@@ -252,17 +341,18 @@ async function installClaudeCode(): Promise<void> {
252
341
  // Atomic write: write to temp file then rename to avoid partial writes
253
342
  const tmpPath = `${claudeJsonPath}.ei-install.tmp`;
254
343
  await Bun.write(tmpPath, JSON.stringify(config, null, 2) + "\n");
255
- const { rename } = await import(/* @vite-ignore */ "fs/promises");
256
344
  await rename(tmpPath, claudeJsonPath);
257
345
 
258
346
  console.log(`✓ Installed Ei MCP server to ${claudeJsonPath}`);
259
347
  console.log(` Restart Claude Code to activate.`);
260
348
 
261
349
  await installClaudeCodeHooks();
350
+
351
+ await installSkillsTo(join(home, ".claude", "skills"));
262
352
  }
263
353
 
264
354
  async function installClaudeCodeHooks(): Promise<void> {
265
- const home = process.env.HOME || "~";
355
+ const home = resolveHome();
266
356
  const hooksDir = join(home, ".claude", "hooks");
267
357
  const scriptPath = join(hooksDir, "ei-inject.ts");
268
358
  const settingsPath = join(home, ".claude", "settings.json");
@@ -340,14 +430,13 @@ The following items MAY be relevant to your current task — use \\\`ei_search\\
340
430
  // Atomic write: write to temp file then rename to avoid partial writes
341
431
  const tmpPath = `${settingsPath}.ei-install.tmp`;
342
432
  await Bun.write(tmpPath, JSON.stringify(settings, null, 2) + "\n");
343
- const { rename } = await import(/* @vite-ignore */ "fs/promises");
344
433
  await rename(tmpPath, settingsPath);
345
434
 
346
435
  console.log(`✓ Installed Ei context hook to ~/.claude/hooks/ei-inject.ts`);
347
436
  }
348
437
 
349
438
  async function installCursor(): Promise<void> {
350
- const home = process.env.HOME || "~";
439
+ const home = resolveHome();
351
440
  const cursorJsonPath = join(home, ".cursor", "mcp.json");
352
441
 
353
442
  // Cursor does not support ${VAR} substitution in mcp.json — literal values only.
@@ -373,7 +462,6 @@ async function installCursor(): Promise<void> {
373
462
  await Bun.$`mkdir -p ${join(home, ".cursor")}`;
374
463
  const tmpPath = `${cursorJsonPath}.ei-install.tmp`;
375
464
  await Bun.write(tmpPath, JSON.stringify(config, null, 2) + "\n");
376
- const { rename } = await import(/* @vite-ignore */ "fs/promises");
377
465
  await rename(tmpPath, cursorJsonPath);
378
466
 
379
467
  console.log(`✓ Installed Ei MCP server to ${cursorJsonPath}`);
@@ -383,7 +471,7 @@ async function installCursor(): Promise<void> {
383
471
  }
384
472
 
385
473
  async function installCursorHooks(): Promise<void> {
386
- const home = process.env.HOME || "~";
474
+ const home = resolveHome();
387
475
  const hooksDir = join(home, ".cursor", "hooks");
388
476
  const rulesDir = join(home, ".cursor", "rules");
389
477
  const hookScriptPath = join(hooksDir, "ei-inject.sh");
@@ -444,14 +532,13 @@ exit 0
444
532
 
445
533
  const tmpPath = `${hooksJsonPath}.ei-install.tmp`;
446
534
  await Bun.write(tmpPath, JSON.stringify(hooksConfig, null, 2) + "\n");
447
- const { rename } = await import(/* @vite-ignore */ "fs/promises");
448
535
  await rename(tmpPath, hooksJsonPath);
449
536
 
450
537
  console.log(`✓ Installed Ei context hook to ~/.cursor/hooks/ei-inject.sh`);
451
538
  }
452
539
 
453
540
  async function installPi(): Promise<void> {
454
- const home = process.env.HOME || "~";
541
+ const home = resolveHome();
455
542
 
456
543
  const extensionContent = `import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
457
544
  import { Type } from "typebox";
@@ -559,8 +646,8 @@ export default function eiIntegration(pi: ExtensionAPI) {
559
646
  console.log(`✓ Installed Ei extension to ~/.pi/agent/extensions/${extFilename}`);
560
647
  }
561
648
 
562
- async function installOmp(): Promise<void> {
563
- const home = process.env.HOME || "~";
649
+ export async function installOmp(): Promise<void> {
650
+ const home = resolveHome();
564
651
 
565
652
  const extensionContent = `import type { ExtensionAPI } from "@oh-my-pi/pi-coding-agent";
566
653
  import { $ } from "bun";
@@ -706,10 +793,12 @@ export default function eiIntegration(pi: ExtensionAPI) {
706
793
  await Bun.$`mkdir -p ${extDir}`;
707
794
  await Bun.write(join(extDir, extFilename), extensionContent);
708
795
  console.log(`✓ Installed Ei extension to ~/.omp/agent/extensions/${extFilename}`);
796
+
797
+ await installSkillsTo(join(home, ".omp", "agent", "skills"));
709
798
  }
710
799
 
711
- async function installOpenCodePlugin(): Promise<void> {
712
- const home = process.env.HOME || "~";
800
+ export async function installOpenCodePlugin(): Promise<void> {
801
+ const home = resolveHome();
713
802
  const opencodeDir = join(home, ".config", "opencode");
714
803
  const pluginsDir = join(opencodeDir, "plugins");
715
804
  const pluginPath = join(pluginsDir, "ei-persona.ts");
@@ -719,11 +808,12 @@ async function installOpenCodePlugin(): Promise<void> {
719
808
  const pluginContent = `import { $ } from "bun"
720
809
  import { join } from "path"
721
810
  import { appendFileSync } from "fs"
811
+ import { homedir } from "os"
722
812
 
723
813
  // Deduplication: the Promise itself is re-awaited on subsequent calls (synchronous once resolved).
724
814
  const personaFetch = new Map<string, Promise<string | null>>()
725
815
 
726
- const logPath = join(process.env.EI_DATA_PATH ?? join(process.env.HOME ?? "~", ".local", "share", "ei"), "ei-persona-plugin.log")
816
+ const logPath = join(process.env.EI_DATA_PATH ?? join(process.env.HOME ?? homedir(), ".local", "share", "ei"), "ei-persona-plugin.log")
727
817
 
728
818
  function log(msg: string) {
729
819
  try {
@@ -793,6 +883,8 @@ export default async function EiPersonaPlugin() {
793
883
  await Bun.write(pluginPath, pluginContent);
794
884
  console.log(`✓ Installed Ei persona plugin to ${pluginPath}`);
795
885
 
886
+ await installSkillsTo(join(opencodeDir, "skills"));
887
+
796
888
  const omoCandidates = [
797
889
  join(opencodeDir, "oh-my-opencode.json"),
798
890
  join(opencodeDir, "oh-my-opencode.jsonc"),
package/src/cli/mcp.ts CHANGED
@@ -6,6 +6,8 @@ import type { StorageState } from "../core/types.js";
6
6
  import type { Message } from "../core/types.js";
7
7
  import type { RoomMessage } from "../core/types/rooms.js";
8
8
  import { resolvePersonaId, filterByPersona, filterTypeSpecificByPersona, filterBySource, filterTypeSpecificBySource } from "./persona-filter.js";
9
+ import { createEntity, updateEntity, removeEntity, CorrectionValidationError } from "./corrections-endpoints.js";
10
+ import { createPersonaEntity, updatePersonaEntity, removePersonaEntity } from "./persona-corrections.js";
9
11
 
10
12
  // Exported so tests can inject their own transport
11
13
  export function createMcpServer(): McpServer {
@@ -99,7 +101,7 @@ export function createMcpServer(): McpServer {
99
101
  "ei_lookup",
100
102
  {
101
103
  description:
102
- "Retrieve the full record for any Ei entity by ID — facts, topics, people, quotes, or personas. Use when ei_search returns an item and you need its complete details (all fields, traits, topics, identifiers, etc.). Pass the entity id from ei_search results.",
104
+ "Retrieve the full record for any Ei entity by ID — facts, topics, people, quotes, or personas. Use when ei_search returns an item and you need its complete details (all fields, traits, topics, identifiers, etc.). Pass the entity id from ei_search results. For facts, topics, and people, the result also includes a `linked_quotes` array listing every quote whose data_item_ids references this entity — check this before correcting a bad merge/split (e.g. un-merging an over-merged Person) to see the full blast radius.",
103
105
  inputSchema: {
104
106
  id: z.string().describe("The entity ID to look up."),
105
107
  source: z
@@ -253,6 +255,83 @@ export function createMcpServer(): McpServer {
253
255
  }
254
256
  );
255
257
 
258
+ server.registerTool(
259
+ "ei_create",
260
+ {
261
+ description:
262
+ "Create a new entity in the user's Ei knowledge base — a fact, topic, person, or persona. Use to add data the extraction pipeline missed, split/correct bad merges, or author a new AI persona's identity (display name, description, traits, topics). The record is validated against the entity type's schema server-side; unknown fields are rejected. Returns the assigned id and the full stored record. Not available for quotes — verifiable-origin data can only be corrected via ei_update, never created.",
263
+ inputSchema: {
264
+ entity_type: z.enum(["fact", "topic", "person", "persona"]).describe("The type of entity to create."),
265
+ data: z
266
+ .record(z.unknown())
267
+ .describe(
268
+ "The entity fields. Fact requires name/description/sentiment/validated_date. Topic requires name/description/sentiment. Person requires a name and/or at least one identifier. Persona requires display_name (traits/topics default to empty arrays, is_paused/is_archived default to false, group_primary/groups_visible default to \"General\" if omitted). Structural validation happens server-side against the entity type's schema."
269
+ ),
270
+ },
271
+ },
272
+ async ({ entity_type, data }) => {
273
+ try {
274
+ if (entity_type === "persona") {
275
+ const { id, record } = await createPersonaEntity(data);
276
+ return { content: [{ type: "text" as const, text: JSON.stringify({ id, record }, null, 2) }] };
277
+ }
278
+ const { id, record } = await createEntity(entity_type, data);
279
+ return { content: [{ type: "text" as const, text: JSON.stringify({ id, record }, null, 2) }] };
280
+ } catch (e) {
281
+ const message = e instanceof CorrectionValidationError ? e.message : (e as Error).message;
282
+ return { content: [{ type: "text" as const, text: `Error: ${message}` }], isError: true };
283
+ }
284
+ }
285
+ );
286
+
287
+ server.registerTool(
288
+ "ei_update",
289
+ {
290
+ description:
291
+ "Replace an existing fact, topic, person, quote, or persona by ID with a COMPLETE record — this is full replacement, not a partial patch. Any field omitted from `data` is treated as absent, not 'leave the existing value alone'. Always fetch the current record with ei_lookup first, edit the fields you need to change, and pass the WHOLE thing back — passing a partial object will silently drop the fields you didn't include. Use to fix bad extracted data (e.g. correcting a person record where two people were wrongly merged into one). Quote records can also be corrected this way — specifically to repoint `data_item_ids` after splitting or merging a person/topic/fact, or to fix mistranscribed `text`. Persona records support full CRUD this way too — rewriting display_name/traits[]/topics[] to author a persona's character (e.g. \"make this persona talk like Yoda\"), or toggling `is_archived` (archive/unarchive is just a normal update). Renaming a persona to a reserved name (\"new\", \"clone\") is rejected.",
292
+ inputSchema: {
293
+ entity_type: z.enum(["fact", "topic", "person", "quote", "persona"]).describe("The type of entity to update."),
294
+ id: z.string().describe("The ID of the entity to replace, from ei_lookup or ei_search."),
295
+ data: z
296
+ .record(z.unknown())
297
+ .describe("The COMPLETE replacement record — round-tripped from ei_lookup output, not a partial patch."),
298
+ },
299
+ },
300
+ async ({ entity_type, id, data }) => {
301
+ try {
302
+ const record = entity_type === "persona" ? await updatePersonaEntity(id, data) : await updateEntity(entity_type, id, data);
303
+ return { content: [{ type: "text" as const, text: JSON.stringify(record, null, 2) }] };
304
+ } catch (e) {
305
+ const message = e instanceof CorrectionValidationError ? e.message : (e as Error).message;
306
+ return { content: [{ type: "text" as const, text: `Error: ${message}` }], isError: true };
307
+ }
308
+ }
309
+ );
310
+
311
+ server.registerTool(
312
+ "ei_remove",
313
+ {
314
+ description:
315
+ "Permanently remove a fact, topic, person, or persona from the user's Ei knowledge base by ID. Use to delete bad extracted data that shouldn't be split or corrected, just dropped entirely, or to delete an AI persona that's no longer needed. Not available for quotes — verifiable-origin data can only be corrected, never deleted. Reserved built-in personas (\"ei\", \"emmet\") cannot be deleted this way — use ei_update to set `is_archived: true` instead.",
316
+ inputSchema: {
317
+ entity_type: z.enum(["fact", "topic", "person", "persona"]).describe("The type of entity to remove."),
318
+ id: z.string().describe("The ID of the entity to remove, from ei_lookup or ei_search."),
319
+ },
320
+ },
321
+ async ({ entity_type, id }) => {
322
+ try {
323
+ if (entity_type === "persona") {
324
+ await removePersonaEntity(id);
325
+ } else {
326
+ await removeEntity(entity_type, id);
327
+ }
328
+ return { content: [{ type: "text" as const, text: JSON.stringify({ removed: true, id }, null, 2) }] };
329
+ } catch (e) {
330
+ return { content: [{ type: "text" as const, text: `Error: ${(e as Error).message}` }], isError: true };
331
+ }
332
+ }
333
+ );
334
+
256
335
  return server;
257
336
  }
258
337