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.
- package/README.md +4 -0
- package/package.json +2 -1
- package/skills/coding-harness-reflect/SKILL.md +230 -0
- package/skills/ei-curate/SKILL.md +174 -0
- package/skills/ei-curate/references/cli.md +160 -0
- package/skills/ei-curate/references/provenance.md +88 -0
- package/skills/ei-curate/references/recipes.md +144 -0
- package/skills/ei-curate/references/talking-to-the-user.md +71 -0
- package/skills/ei-persona/SKILL.md +238 -0
- package/skills/ei-persona/references/cli.md +265 -0
- package/skills/ei-persona/references/recipes.md +247 -0
- package/skills/ei-persona/references/talking-to-the-user.md +107 -0
- package/src/cli/README.md +72 -2
- package/src/cli/corrections-endpoints.ts +297 -0
- package/src/cli/corrections-writer.ts +138 -0
- package/src/cli/install.ts +117 -25
- package/src/cli/mcp.ts +80 -1
- package/src/cli/persona-corrections.ts +442 -0
- package/src/cli/retrieval.ts +46 -2
- package/src/cli.ts +131 -1
- package/src/core/corrections.ts +233 -0
- package/src/core/handlers/human-extraction.ts +64 -15
- package/src/core/handlers/human-matching.ts +2 -2
- package/src/core/orchestrators/human-extraction.ts +1 -11
- package/src/core/persona-manager.ts +18 -7
- package/src/core/persona-tools.ts +92 -0
- package/src/core/processor.ts +113 -1
- package/src/core/state/human.ts +10 -0
- package/src/core/state/personas.ts +8 -0
- package/src/core/state-manager.ts +11 -0
- package/src/core/utils/identifier-utils.ts +3 -2
- package/src/prompts/human/person-scan.ts +2 -0
- package/src/prompts/human/person-update.ts +14 -3
- package/src/storage/file-lock.ts +120 -0
- package/tui/README.md +2 -0
- package/tui/src/commands/provider.tsx +1 -1
- package/tui/src/components/WelcomeOverlay.tsx +3 -3
- package/tui/src/context/ei.tsx +14 -0
- package/tui/src/index.tsx +13 -1
- package/tui/src/storage/file.ts +15 -83
- package/tui/src/util/instance-lock.ts +3 -2
- package/tui/src/util/yaml-persona.ts +7 -38
package/src/cli/install.ts
CHANGED
|
@@ -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
|
-
|
|
86
|
+
const failures: string[] = [];
|
|
87
|
+
|
|
88
|
+
if (!(await runInstallStep("Claude Code", installClaudeCode))) failures.push("Claude Code");
|
|
5
89
|
|
|
6
|
-
const 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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ??
|
|
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
|
|