ei-tui 0.1.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/LICENSE +21 -0
- package/README.md +170 -0
- package/package.json +63 -0
- package/src/README.md +96 -0
- package/src/cli/README.md +47 -0
- package/src/cli/commands/facts.ts +25 -0
- package/src/cli/commands/people.ts +25 -0
- package/src/cli/commands/quotes.ts +19 -0
- package/src/cli/commands/topics.ts +25 -0
- package/src/cli/commands/traits.ts +25 -0
- package/src/cli/retrieval.ts +269 -0
- package/src/cli.ts +176 -0
- package/src/core/AGENTS.md +104 -0
- package/src/core/embedding-service.ts +241 -0
- package/src/core/handlers/index.ts +1057 -0
- package/src/core/index.ts +4 -0
- package/src/core/llm-client.ts +265 -0
- package/src/core/model-context-windows.ts +49 -0
- package/src/core/orchestrators/ceremony.ts +500 -0
- package/src/core/orchestrators/extraction-chunker.ts +138 -0
- package/src/core/orchestrators/human-extraction.ts +457 -0
- package/src/core/orchestrators/index.ts +28 -0
- package/src/core/orchestrators/persona-generation.ts +76 -0
- package/src/core/orchestrators/persona-topics.ts +117 -0
- package/src/core/personas/index.ts +5 -0
- package/src/core/personas/opencode-agent.ts +81 -0
- package/src/core/processor.ts +1413 -0
- package/src/core/queue-processor.ts +197 -0
- package/src/core/state/checkpoints.ts +68 -0
- package/src/core/state/human.ts +176 -0
- package/src/core/state/index.ts +5 -0
- package/src/core/state/personas.ts +217 -0
- package/src/core/state/queue.ts +144 -0
- package/src/core/state-manager.ts +347 -0
- package/src/core/types.ts +421 -0
- package/src/core/utils/decay.ts +33 -0
- package/src/index.ts +1 -0
- package/src/integrations/opencode/importer.ts +896 -0
- package/src/integrations/opencode/index.ts +16 -0
- package/src/integrations/opencode/json-reader.ts +304 -0
- package/src/integrations/opencode/reader-factory.ts +35 -0
- package/src/integrations/opencode/sqlite-reader.ts +189 -0
- package/src/integrations/opencode/types.ts +244 -0
- package/src/prompts/AGENTS.md +62 -0
- package/src/prompts/ceremony/description-check.ts +47 -0
- package/src/prompts/ceremony/expire.ts +30 -0
- package/src/prompts/ceremony/explore.ts +60 -0
- package/src/prompts/ceremony/index.ts +11 -0
- package/src/prompts/ceremony/types.ts +42 -0
- package/src/prompts/generation/descriptions.ts +91 -0
- package/src/prompts/generation/index.ts +15 -0
- package/src/prompts/generation/persona.ts +155 -0
- package/src/prompts/generation/seeds.ts +31 -0
- package/src/prompts/generation/types.ts +47 -0
- package/src/prompts/heartbeat/check.ts +179 -0
- package/src/prompts/heartbeat/ei.ts +208 -0
- package/src/prompts/heartbeat/index.ts +15 -0
- package/src/prompts/heartbeat/types.ts +70 -0
- package/src/prompts/human/fact-scan.ts +152 -0
- package/src/prompts/human/index.ts +32 -0
- package/src/prompts/human/item-match.ts +74 -0
- package/src/prompts/human/item-update.ts +322 -0
- package/src/prompts/human/person-scan.ts +115 -0
- package/src/prompts/human/topic-scan.ts +135 -0
- package/src/prompts/human/trait-scan.ts +115 -0
- package/src/prompts/human/types.ts +127 -0
- package/src/prompts/index.ts +90 -0
- package/src/prompts/message-utils.ts +39 -0
- package/src/prompts/persona/index.ts +16 -0
- package/src/prompts/persona/topics-match.ts +69 -0
- package/src/prompts/persona/topics-scan.ts +98 -0
- package/src/prompts/persona/topics-update.ts +157 -0
- package/src/prompts/persona/traits.ts +117 -0
- package/src/prompts/persona/types.ts +74 -0
- package/src/prompts/response/index.ts +147 -0
- package/src/prompts/response/sections.ts +355 -0
- package/src/prompts/response/types.ts +38 -0
- package/src/prompts/validation/ei.ts +93 -0
- package/src/prompts/validation/index.ts +6 -0
- package/src/prompts/validation/types.ts +22 -0
- package/src/storage/crypto.ts +96 -0
- package/src/storage/index.ts +5 -0
- package/src/storage/interface.ts +9 -0
- package/src/storage/local.ts +79 -0
- package/src/storage/merge.ts +69 -0
- package/src/storage/remote.ts +145 -0
- package/src/templates/welcome.ts +91 -0
- package/tui/README.md +62 -0
- package/tui/bunfig.toml +4 -0
- package/tui/src/app.tsx +55 -0
- package/tui/src/commands/archive.tsx +93 -0
- package/tui/src/commands/context.tsx +124 -0
- package/tui/src/commands/delete.tsx +71 -0
- package/tui/src/commands/details.tsx +41 -0
- package/tui/src/commands/editor.tsx +46 -0
- package/tui/src/commands/help.tsx +12 -0
- package/tui/src/commands/me.tsx +145 -0
- package/tui/src/commands/model.ts +47 -0
- package/tui/src/commands/new.ts +31 -0
- package/tui/src/commands/pause.ts +46 -0
- package/tui/src/commands/persona.tsx +58 -0
- package/tui/src/commands/provider.tsx +124 -0
- package/tui/src/commands/quit.ts +22 -0
- package/tui/src/commands/quotes.tsx +172 -0
- package/tui/src/commands/registry.test.ts +137 -0
- package/tui/src/commands/registry.ts +130 -0
- package/tui/src/commands/resume.ts +39 -0
- package/tui/src/commands/setsync.tsx +43 -0
- package/tui/src/commands/settings.tsx +83 -0
- package/tui/src/components/ConfirmOverlay.tsx +51 -0
- package/tui/src/components/ConflictOverlay.tsx +78 -0
- package/tui/src/components/HelpOverlay.tsx +69 -0
- package/tui/src/components/Layout.tsx +24 -0
- package/tui/src/components/MessageList.tsx +174 -0
- package/tui/src/components/PersonaListOverlay.tsx +186 -0
- package/tui/src/components/PromptInput.tsx +145 -0
- package/tui/src/components/ProviderListOverlay.tsx +208 -0
- package/tui/src/components/QuotesOverlay.tsx +157 -0
- package/tui/src/components/Sidebar.tsx +95 -0
- package/tui/src/components/StatusBar.tsx +77 -0
- package/tui/src/components/WelcomeOverlay.tsx +73 -0
- package/tui/src/context/ei.tsx +623 -0
- package/tui/src/context/keyboard.tsx +164 -0
- package/tui/src/context/overlay.tsx +53 -0
- package/tui/src/index.tsx +8 -0
- package/tui/src/storage/file.ts +185 -0
- package/tui/src/util/duration.ts +32 -0
- package/tui/src/util/editor.ts +188 -0
- package/tui/src/util/logger.ts +109 -0
- package/tui/src/util/persona-editor.tsx +181 -0
- package/tui/src/util/provider-editor.tsx +168 -0
- package/tui/src/util/syntax.ts +35 -0
- package/tui/src/util/yaml-serializers.ts +755 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import type { Command } from "./registry.js";
|
|
2
|
+
import { spawnEditor } from "../util/editor.js";
|
|
3
|
+
import { contextToYAML, contextFromYAML } from "../util/yaml-serializers.js";
|
|
4
|
+
import { logger } from "../util/logger.js";
|
|
5
|
+
import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
|
|
6
|
+
|
|
7
|
+
export const contextCommand: Command = {
|
|
8
|
+
name: "context",
|
|
9
|
+
aliases: ["messages"],
|
|
10
|
+
description: "Edit message context status in $EDITOR",
|
|
11
|
+
usage: "/context",
|
|
12
|
+
|
|
13
|
+
async execute(_args, ctx) {
|
|
14
|
+
const personaId = ctx.ei.activePersonaId();
|
|
15
|
+
if (!personaId) {
|
|
16
|
+
ctx.showNotification("No active persona", "error");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const messages = ctx.ei.messages();
|
|
21
|
+
if (messages.length === 0) {
|
|
22
|
+
ctx.showNotification("No messages to edit", "info");
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const originalStatus = new Map(messages.map((m) => [m.id, m.context_status]));
|
|
27
|
+
|
|
28
|
+
let yamlContent = contextToYAML(messages);
|
|
29
|
+
let editorIteration = 0;
|
|
30
|
+
|
|
31
|
+
while (true) {
|
|
32
|
+
editorIteration++;
|
|
33
|
+
logger.debug("[context] starting editor iteration", { iteration: editorIteration });
|
|
34
|
+
|
|
35
|
+
const result = await spawnEditor({
|
|
36
|
+
initialContent: yamlContent,
|
|
37
|
+
filename: "context.yaml",
|
|
38
|
+
renderer: ctx.renderer,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
logger.debug("[context] editor returned", {
|
|
42
|
+
iteration: editorIteration,
|
|
43
|
+
aborted: result.aborted,
|
|
44
|
+
success: result.success,
|
|
45
|
+
hasContent: result.content !== null,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
if (result.aborted) {
|
|
49
|
+
ctx.showNotification("Editor cancelled", "info");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!result.success) {
|
|
54
|
+
ctx.showNotification("Editor failed to open", "error");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (result.content === null) {
|
|
59
|
+
ctx.showNotification("No changes made", "info");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const parsed = contextFromYAML(result.content);
|
|
65
|
+
|
|
66
|
+
if (parsed.deletedMessageIds.length > 0) {
|
|
67
|
+
await ctx.ei.deleteMessages(personaId, parsed.deletedMessageIds);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const msg of parsed.messages) {
|
|
71
|
+
const orig = originalStatus.get(msg.id);
|
|
72
|
+
if (orig !== undefined && orig !== msg.context_status) {
|
|
73
|
+
await ctx.ei.setMessageContextStatus(personaId, msg.id, msg.context_status);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const deleteCount = parsed.deletedMessageIds.length;
|
|
78
|
+
const notification =
|
|
79
|
+
deleteCount > 0
|
|
80
|
+
? `Context updated (${deleteCount} message${deleteCount === 1 ? "" : "s"} deleted)`
|
|
81
|
+
: "Context updated";
|
|
82
|
+
|
|
83
|
+
ctx.showNotification(notification, "info");
|
|
84
|
+
return;
|
|
85
|
+
} catch (parseError) {
|
|
86
|
+
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
|
87
|
+
logger.debug("[context] YAML parse error, prompting for re-edit", {
|
|
88
|
+
iteration: editorIteration,
|
|
89
|
+
error: errorMsg,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const shouldReEdit = await new Promise<boolean>((resolve) => {
|
|
93
|
+
ctx.showOverlay((hideOverlay) => (
|
|
94
|
+
<ConfirmOverlay
|
|
95
|
+
message={`YAML parse error:\n${errorMsg}\n\nRe-edit?`}
|
|
96
|
+
onConfirm={() => {
|
|
97
|
+
logger.debug("[context] user confirmed re-edit");
|
|
98
|
+
hideOverlay();
|
|
99
|
+
resolve(true);
|
|
100
|
+
}}
|
|
101
|
+
onCancel={() => {
|
|
102
|
+
logger.debug("[context] user cancelled re-edit");
|
|
103
|
+
hideOverlay();
|
|
104
|
+
resolve(false);
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
));
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
logger.debug("[context] shouldReEdit", { shouldReEdit, iteration: editorIteration });
|
|
111
|
+
|
|
112
|
+
if (shouldReEdit) {
|
|
113
|
+
yamlContent = result.content;
|
|
114
|
+
logger.debug("[context] continuing to next iteration");
|
|
115
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
116
|
+
continue;
|
|
117
|
+
} else {
|
|
118
|
+
ctx.showNotification("Changes discarded", "info");
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
import { PersonaListOverlay } from "../components/PersonaListOverlay";
|
|
3
|
+
import { ConfirmOverlay } from "../components/ConfirmOverlay";
|
|
4
|
+
|
|
5
|
+
export const deleteCommand: Command = {
|
|
6
|
+
name: "delete",
|
|
7
|
+
aliases: ["del"],
|
|
8
|
+
description: "*Permanently* delete a persona",
|
|
9
|
+
usage: "/delete [name]",
|
|
10
|
+
|
|
11
|
+
async execute(args, ctx) {
|
|
12
|
+
const allPersonas = ctx.ei.personas();
|
|
13
|
+
const deletable = allPersonas.filter(p => p.id !== ctx.ei.activePersonaId());
|
|
14
|
+
|
|
15
|
+
const confirmAndDelete = async (personaId: string, displayName: string) => {
|
|
16
|
+
const confirmed = await new Promise<boolean>((resolve) => {
|
|
17
|
+
ctx.showOverlay((hideOverlay) => (
|
|
18
|
+
<ConfirmOverlay
|
|
19
|
+
message={`Delete "${displayName}"?\nThis cannot be undone.`}
|
|
20
|
+
onConfirm={() => { hideOverlay(); resolve(true); }}
|
|
21
|
+
onCancel={() => { hideOverlay(); resolve(false); }}
|
|
22
|
+
/>
|
|
23
|
+
));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (confirmed) {
|
|
27
|
+
await ctx.ei.deletePersona(personaId);
|
|
28
|
+
ctx.showNotification(`Deleted ${displayName}`, "info");
|
|
29
|
+
} else {
|
|
30
|
+
ctx.showNotification("Cancelled", "info");
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (args.length === 0) {
|
|
35
|
+
if (deletable.length === 0) {
|
|
36
|
+
ctx.showNotification("No personas available to delete", "info");
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
ctx.showOverlay((hideOverlay) => (
|
|
40
|
+
<PersonaListOverlay
|
|
41
|
+
personas={deletable}
|
|
42
|
+
activePersonaId={null}
|
|
43
|
+
title="Select persona to delete"
|
|
44
|
+
onSelect={async (personaId) => {
|
|
45
|
+
const persona = deletable.find(p => p.id === personaId);
|
|
46
|
+
hideOverlay();
|
|
47
|
+
await confirmAndDelete(personaId, persona?.display_name ?? personaId);
|
|
48
|
+
}}
|
|
49
|
+
onDismiss={hideOverlay}
|
|
50
|
+
/>
|
|
51
|
+
));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const nameOrAlias = args.join(" ");
|
|
56
|
+
const personaId = await ctx.ei.resolvePersonaName(nameOrAlias);
|
|
57
|
+
|
|
58
|
+
if (!personaId) {
|
|
59
|
+
ctx.showNotification(`Persona '${nameOrAlias}' not found`, "error");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (personaId === ctx.ei.activePersonaId()) {
|
|
64
|
+
ctx.showNotification("Cannot delete active persona. Switch to another first.", "error");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const persona = allPersonas.find(p => p.id === personaId);
|
|
69
|
+
await confirmAndDelete(personaId, persona?.display_name ?? nameOrAlias);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Command } from "./registry.js";
|
|
2
|
+
import { openPersonaEditor } from "../util/persona-editor.js";
|
|
3
|
+
|
|
4
|
+
export const detailsCommand: Command = {
|
|
5
|
+
name: "details",
|
|
6
|
+
aliases: ["d"],
|
|
7
|
+
description: "Edit persona details in $EDITOR",
|
|
8
|
+
usage: "/details [persona] - Edit specified or current persona",
|
|
9
|
+
|
|
10
|
+
async execute(args, ctx) {
|
|
11
|
+
let personaId: string | null;
|
|
12
|
+
|
|
13
|
+
if (args.length > 0) {
|
|
14
|
+
const nameOrAlias = args.join(" ");
|
|
15
|
+
personaId = await ctx.ei.resolvePersonaName(nameOrAlias);
|
|
16
|
+
if (!personaId) {
|
|
17
|
+
ctx.showNotification(`Persona "${nameOrAlias}" not found`, "error");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
personaId = ctx.ei.activePersonaId();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!personaId) {
|
|
25
|
+
ctx.showNotification("No active persona", "error");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const persona = await ctx.ei.getPersona(personaId);
|
|
30
|
+
if (!persona) {
|
|
31
|
+
ctx.showNotification(`Persona not found`, "error");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
await openPersonaEditor({
|
|
36
|
+
personaId,
|
|
37
|
+
persona,
|
|
38
|
+
ctx,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Command } from "./registry.js";
|
|
2
|
+
import { spawnEditor } from "../util/editor.js";
|
|
3
|
+
|
|
4
|
+
export const editorCommand: Command = {
|
|
5
|
+
name: "editor",
|
|
6
|
+
aliases: ["e", "edit"],
|
|
7
|
+
description: "Open $EDITOR",
|
|
8
|
+
usage: "/editor - Opens editor (Ctrl+E preserves current input)",
|
|
9
|
+
|
|
10
|
+
async execute(_args, ctx) {
|
|
11
|
+
if (!ctx.renderer) {
|
|
12
|
+
ctx.showNotification("Editor not available", "error");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let currentText = ctx.getInputText();
|
|
17
|
+
|
|
18
|
+
if (currentText.startsWith("/editor") || currentText.startsWith("/edit") || currentText.startsWith("/e")) {
|
|
19
|
+
currentText = "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = await spawnEditor({
|
|
23
|
+
initialContent: currentText,
|
|
24
|
+
filename: "message.txt",
|
|
25
|
+
renderer: ctx.renderer,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (result.aborted) {
|
|
29
|
+
ctx.showNotification("Editor cancelled", "info");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!result.success) {
|
|
34
|
+
ctx.showNotification("Editor failed to open", "error");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (result.content === null) {
|
|
39
|
+
ctx.showNotification("No changes made", "info");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
ctx.setInputText(result.content.trimEnd());
|
|
44
|
+
ctx.showNotification("Input updated from editor", "info");
|
|
45
|
+
}
|
|
46
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
import { HelpOverlay } from "../components/HelpOverlay";
|
|
3
|
+
|
|
4
|
+
export const helpCommand: Command = {
|
|
5
|
+
name: "help",
|
|
6
|
+
aliases: ["h"],
|
|
7
|
+
description: "Show help screen",
|
|
8
|
+
usage: "/help or /h",
|
|
9
|
+
execute: async (_args, ctx) => {
|
|
10
|
+
ctx.showOverlay((hideOverlay) => <HelpOverlay onDismiss={hideOverlay} />);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { Command } from "./registry.js";
|
|
2
|
+
import { spawnEditor } from "../util/editor.js";
|
|
3
|
+
import { humanToYAML, humanFromYAML } from "../util/yaml-serializers.js";
|
|
4
|
+
import { logger } from "../util/logger.js";
|
|
5
|
+
import { ConfirmOverlay } from "../components/ConfirmOverlay.js";
|
|
6
|
+
|
|
7
|
+
type DataType = "facts" | "traits" | "topics" | "people";
|
|
8
|
+
|
|
9
|
+
const VALID_TYPES: DataType[] = ["facts", "traits", "topics", "people"];
|
|
10
|
+
|
|
11
|
+
export const meCommand: Command = {
|
|
12
|
+
name: "me",
|
|
13
|
+
aliases: [],
|
|
14
|
+
description: "Edit your data in $EDITOR",
|
|
15
|
+
usage: "/me [facts|traits|topics|people]",
|
|
16
|
+
|
|
17
|
+
async execute(args, ctx) {
|
|
18
|
+
const human = await ctx.ei.getHuman();
|
|
19
|
+
|
|
20
|
+
const filterArg = args[0]?.toLowerCase();
|
|
21
|
+
const filterType: DataType | null = filterArg && VALID_TYPES.includes(filterArg as DataType)
|
|
22
|
+
? filterArg as DataType
|
|
23
|
+
: null;
|
|
24
|
+
|
|
25
|
+
if (filterArg && !filterType) {
|
|
26
|
+
ctx.showNotification(`Invalid type: ${filterArg}. Use: facts, traits, topics, people`, "error");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const filteredHuman = filterType ? {
|
|
31
|
+
...human,
|
|
32
|
+
facts: filterType === "facts" ? human.facts : [],
|
|
33
|
+
traits: filterType === "traits" ? human.traits : [],
|
|
34
|
+
topics: filterType === "topics" ? human.topics : [],
|
|
35
|
+
people: filterType === "people" ? human.people : [],
|
|
36
|
+
} : human;
|
|
37
|
+
|
|
38
|
+
let yamlContent = humanToYAML(filteredHuman);
|
|
39
|
+
let editorIteration = 0;
|
|
40
|
+
|
|
41
|
+
while (true) {
|
|
42
|
+
editorIteration++;
|
|
43
|
+
logger.debug("[me] starting editor iteration", { iteration: editorIteration });
|
|
44
|
+
|
|
45
|
+
const result = await spawnEditor({
|
|
46
|
+
initialContent: yamlContent,
|
|
47
|
+
filename: "human-data.yaml",
|
|
48
|
+
renderer: ctx.renderer,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
logger.debug("[me] editor returned", { iteration: editorIteration, aborted: result.aborted, success: result.success, hasContent: result.content !== null });
|
|
52
|
+
|
|
53
|
+
if (result.aborted) {
|
|
54
|
+
ctx.showNotification("Editor cancelled", "info");
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
ctx.showNotification("Editor failed to open", "error");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (result.content === null) {
|
|
64
|
+
ctx.showNotification("No changes made", "info");
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const parsed = humanFromYAML(result.content);
|
|
70
|
+
|
|
71
|
+
for (const id of parsed.deletedFactIds) {
|
|
72
|
+
await ctx.ei.removeDataItem("fact", id);
|
|
73
|
+
}
|
|
74
|
+
for (const id of parsed.deletedTraitIds) {
|
|
75
|
+
await ctx.ei.removeDataItem("trait", id);
|
|
76
|
+
}
|
|
77
|
+
for (const id of parsed.deletedTopicIds) {
|
|
78
|
+
await ctx.ei.removeDataItem("topic", id);
|
|
79
|
+
}
|
|
80
|
+
for (const id of parsed.deletedPersonIds) {
|
|
81
|
+
await ctx.ei.removeDataItem("person", id);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const fact of parsed.facts) {
|
|
85
|
+
await ctx.ei.upsertFact(fact);
|
|
86
|
+
}
|
|
87
|
+
for (const trait of parsed.traits) {
|
|
88
|
+
await ctx.ei.upsertTrait(trait);
|
|
89
|
+
}
|
|
90
|
+
for (const topic of parsed.topics) {
|
|
91
|
+
await ctx.ei.upsertTopic(topic);
|
|
92
|
+
}
|
|
93
|
+
for (const person of parsed.people) {
|
|
94
|
+
await ctx.ei.upsertPerson(person);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const deleteCount = parsed.deletedFactIds.length +
|
|
98
|
+
parsed.deletedTraitIds.length +
|
|
99
|
+
parsed.deletedTopicIds.length +
|
|
100
|
+
parsed.deletedPersonIds.length;
|
|
101
|
+
const updateCount = parsed.facts.length +
|
|
102
|
+
parsed.traits.length +
|
|
103
|
+
parsed.topics.length +
|
|
104
|
+
parsed.people.length;
|
|
105
|
+
|
|
106
|
+
ctx.showNotification(`Updated ${updateCount} items, deleted ${deleteCount}`, "info");
|
|
107
|
+
return;
|
|
108
|
+
|
|
109
|
+
} catch (parseError) {
|
|
110
|
+
const errorMsg = parseError instanceof Error ? parseError.message : String(parseError);
|
|
111
|
+
logger.debug("[me] YAML parse error, prompting for re-edit", { iteration: editorIteration, error: errorMsg });
|
|
112
|
+
|
|
113
|
+
const shouldReEdit = await new Promise<boolean>((resolve) => {
|
|
114
|
+
ctx.showOverlay((hideOverlay) => (
|
|
115
|
+
<ConfirmOverlay
|
|
116
|
+
message={`YAML parse error:\n${errorMsg}\n\nRe-edit?`}
|
|
117
|
+
onConfirm={() => {
|
|
118
|
+
logger.debug("[me] user confirmed re-edit");
|
|
119
|
+
hideOverlay();
|
|
120
|
+
resolve(true);
|
|
121
|
+
}}
|
|
122
|
+
onCancel={() => {
|
|
123
|
+
logger.debug("[me] user cancelled re-edit");
|
|
124
|
+
hideOverlay();
|
|
125
|
+
resolve(false);
|
|
126
|
+
}}
|
|
127
|
+
/>
|
|
128
|
+
));
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
logger.debug("[me] shouldReEdit", { shouldReEdit, iteration: editorIteration });
|
|
132
|
+
|
|
133
|
+
if (shouldReEdit) {
|
|
134
|
+
yamlContent = result.content;
|
|
135
|
+
logger.debug("[me] continuing to next iteration");
|
|
136
|
+
await new Promise(r => setTimeout(r, 50));
|
|
137
|
+
continue;
|
|
138
|
+
} else {
|
|
139
|
+
ctx.showNotification("Changes discarded", "info");
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
|
|
3
|
+
export const modelCommand: Command = {
|
|
4
|
+
name: "model",
|
|
5
|
+
aliases: [],
|
|
6
|
+
description: "Set the LLM model for the current persona",
|
|
7
|
+
usage: "/model <model> or /model <provider:model>",
|
|
8
|
+
execute: async (args, ctx) => {
|
|
9
|
+
const personaId = ctx.ei.activePersonaId();
|
|
10
|
+
if (!personaId) {
|
|
11
|
+
ctx.showNotification("No persona selected", "error");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (args.length === 0) {
|
|
16
|
+
ctx.showNotification("Usage: /model <model> (e.g., sonnet-latest or openai:gpt-4o)", "info");
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const modelSpec = args[0];
|
|
21
|
+
|
|
22
|
+
if (modelSpec.includes(":")) {
|
|
23
|
+
// Explicit provider:model — use as-is
|
|
24
|
+
await ctx.ei.updatePersona(personaId, { model: modelSpec });
|
|
25
|
+
ctx.showNotification(`Model set to ${modelSpec}`, "info");
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// No provider specified — infer from persona's current model
|
|
30
|
+
const persona = await ctx.ei.getPersona(personaId);
|
|
31
|
+
const currentModel = persona?.model;
|
|
32
|
+
|
|
33
|
+
if (currentModel) {
|
|
34
|
+
const provider = currentModel.includes(":")
|
|
35
|
+
? currentModel.split(":")[0]
|
|
36
|
+
: currentModel;
|
|
37
|
+
const newModel = `${provider}:${modelSpec}`;
|
|
38
|
+
await ctx.ei.updatePersona(personaId, { model: newModel });
|
|
39
|
+
ctx.showNotification(`Model set to ${newModel}`, "info");
|
|
40
|
+
} else {
|
|
41
|
+
ctx.showNotification(
|
|
42
|
+
"No provider set. Use /provider first, or specify provider:model (e.g., openai:gpt-4o)",
|
|
43
|
+
"error"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
|
|
3
|
+
export const newCommand: Command = {
|
|
4
|
+
name: "new",
|
|
5
|
+
aliases: [],
|
|
6
|
+
description: "Toggle context boundary (new convo)",
|
|
7
|
+
usage: "/new",
|
|
8
|
+
execute: async (_args, ctx) => {
|
|
9
|
+
const personaId = ctx.ei.activePersonaId();
|
|
10
|
+
if (!personaId) {
|
|
11
|
+
ctx.showNotification("No persona selected", "error");
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const persona = ctx.ei.personas().find(p => p.id === personaId);
|
|
16
|
+
const messages = ctx.ei.messages();
|
|
17
|
+
const lastMessage = messages[messages.length - 1];
|
|
18
|
+
|
|
19
|
+
const boundaryIsActive = persona?.context_boundary &&
|
|
20
|
+
(!lastMessage || persona.context_boundary > lastMessage.timestamp);
|
|
21
|
+
|
|
22
|
+
if (boundaryIsActive) {
|
|
23
|
+
await ctx.ei.setContextBoundary(personaId, null);
|
|
24
|
+
ctx.showNotification("Context boundary cleared - previous messages restored", "info");
|
|
25
|
+
} else {
|
|
26
|
+
const timestamp = new Date().toISOString();
|
|
27
|
+
await ctx.ei.setContextBoundary(personaId, timestamp);
|
|
28
|
+
ctx.showNotification("Context boundary set - conversation starts fresh", "info");
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
import { parseDuration, formatDuration } from "../util/duration";
|
|
3
|
+
|
|
4
|
+
export const pauseCommand: Command = {
|
|
5
|
+
name: "pause",
|
|
6
|
+
aliases: [],
|
|
7
|
+
description: "Pause persona (optionally for a duration)",
|
|
8
|
+
usage: "/pause [duration] - e.g., /pause 2h, /pause 1d",
|
|
9
|
+
execute: async (args, ctx) => {
|
|
10
|
+
const personaId = ctx.ei.activePersonaId();
|
|
11
|
+
if (!personaId) {
|
|
12
|
+
ctx.showNotification("No persona selected", "error");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const personas = ctx.ei.personas();
|
|
17
|
+
const persona = personas.find(p => p.id === personaId);
|
|
18
|
+
const visibleActive = personas.filter(p => !p.is_archived && !p.is_paused);
|
|
19
|
+
|
|
20
|
+
if (visibleActive.length <= 1) {
|
|
21
|
+
ctx.showNotification("Cannot pause - at least one persona must remain active", "error");
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const displayName = persona?.display_name ?? personaId;
|
|
26
|
+
let pauseUntil: string;
|
|
27
|
+
let message: string;
|
|
28
|
+
|
|
29
|
+
if (args.length > 0) {
|
|
30
|
+
const durationMs = parseDuration(args[0]);
|
|
31
|
+
if (!durationMs) {
|
|
32
|
+
ctx.showNotification(`Invalid duration: ${args[0]}. Use formats like 2h, 1d, 1w`, "error");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const resumeTime = new Date(Date.now() + durationMs);
|
|
36
|
+
pauseUntil = resumeTime.toISOString();
|
|
37
|
+
message = `Paused ${displayName} for ${formatDuration(durationMs)}`;
|
|
38
|
+
} else {
|
|
39
|
+
pauseUntil = "0";
|
|
40
|
+
message = `Paused ${displayName} indefinitely`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await ctx.ei.updatePersona(personaId, { is_paused: true, pause_until: pauseUntil });
|
|
44
|
+
ctx.showNotification(message, "info");
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Command } from "./registry";
|
|
2
|
+
import { isReservedPersonaName } from "../../../src/core/types.js";
|
|
3
|
+
import { PersonaListOverlay } from "../components/PersonaListOverlay";
|
|
4
|
+
import { createPersonaViaEditor } from "../util/persona-editor.js";
|
|
5
|
+
|
|
6
|
+
export const personaCommand: Command = {
|
|
7
|
+
name: "persona",
|
|
8
|
+
aliases: ["p"],
|
|
9
|
+
description: "Switch persona, list all, or create new",
|
|
10
|
+
usage: "/persona [name] | /persona new <name>",
|
|
11
|
+
|
|
12
|
+
async execute(args, ctx) {
|
|
13
|
+
const unarchived = ctx.ei.personas().filter(p => !p.is_archived);
|
|
14
|
+
|
|
15
|
+
if (args.length === 0) {
|
|
16
|
+
ctx.showOverlay((hideOverlay) => (
|
|
17
|
+
<PersonaListOverlay
|
|
18
|
+
personas={unarchived}
|
|
19
|
+
activePersonaId={ctx.ei.activePersonaId()}
|
|
20
|
+
onSelect={(personaId) => {
|
|
21
|
+
const persona = unarchived.find(p => p.id === personaId);
|
|
22
|
+
ctx.ei.selectPersona(personaId);
|
|
23
|
+
hideOverlay();
|
|
24
|
+
ctx.showNotification(`Switched to ${persona?.display_name ?? personaId}`, "info");
|
|
25
|
+
}}
|
|
26
|
+
onDismiss={hideOverlay}
|
|
27
|
+
/>
|
|
28
|
+
));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (args[0].toLowerCase() === "new") {
|
|
33
|
+
if (args.length < 2) {
|
|
34
|
+
ctx.showNotification("Usage: /p new <name>", "error");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const personaName = args.slice(1).join(" ");
|
|
38
|
+
if (isReservedPersonaName(personaName)) {
|
|
39
|
+
ctx.showNotification(`Cannot use reserved name "${personaName}"`, "error");
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await createPersonaViaEditor({ personaName, ctx });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// User typed a name - resolve it to ID, then switch
|
|
47
|
+
const nameOrAlias = args.join(" ");
|
|
48
|
+
const personaId = await ctx.ei.resolvePersonaName(nameOrAlias);
|
|
49
|
+
|
|
50
|
+
if (personaId) {
|
|
51
|
+
const persona = unarchived.find(p => p.id === personaId);
|
|
52
|
+
ctx.ei.selectPersona(personaId);
|
|
53
|
+
ctx.showNotification(`Switched to ${persona?.display_name ?? nameOrAlias}`, "info");
|
|
54
|
+
} else {
|
|
55
|
+
ctx.showNotification(`No persona named "${nameOrAlias}". Run \`/p new ${nameOrAlias}\` to create.`, "warn");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|