ei-tui 0.6.3 → 0.6.5
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 +1 -1
- package/src/core/handlers/dedup.ts +4 -1
- package/src/core/handlers/human-matching.ts +33 -15
- package/src/core/handlers/index.ts +1 -0
- package/src/core/orchestrators/human-extraction.ts +72 -11
- package/src/core/orchestrators/index.ts +2 -0
- package/src/core/types/enums.ts +1 -0
- package/src/prompts/ceremony/dedup.ts +89 -1
- package/src/prompts/ceremony/index.ts +2 -1
- package/src/prompts/ceremony/types.ts +8 -0
- package/tui/src/commands/editor.tsx +3 -0
- package/tui/src/commands/help.tsx +1 -1
- package/tui/src/commands/me.tsx +52 -16
- package/tui/src/components/HelpOverlay.tsx +63 -33
- package/tui/src/context/overlay.tsx +2 -0
- package/tui/src/index.tsx +7 -0
- package/tui/src/util/editor.ts +36 -3
- package/tui/src/util/help-content.ts +136 -0
- package/tui/src/util/yaml-human.ts +129 -34
- package/tui/src/util/yaml-persona.ts +11 -10
- package/tui/src/util/yaml-settings.ts +21 -15
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { useKeyboard } from "@opentui/solid";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { onMount, onCleanup } from "solid-js";
|
|
3
|
+
import type { CliRenderer } from "@opentui/core";
|
|
4
4
|
import { useKeyboardNav } from "../context/keyboard.js";
|
|
5
|
+
import { spawnPager } from "../util/editor.js";
|
|
6
|
+
import { buildManPage } from "../util/help-content.js";
|
|
5
7
|
|
|
6
8
|
interface HelpOverlayProps {
|
|
7
9
|
onDismiss: () => void;
|
|
10
|
+
renderer: CliRenderer;
|
|
8
11
|
}
|
|
9
12
|
|
|
10
13
|
export function HelpOverlay(props: HelpOverlayProps) {
|
|
@@ -14,11 +17,14 @@ export function HelpOverlay(props: HelpOverlayProps) {
|
|
|
14
17
|
|
|
15
18
|
useKeyboard((event) => {
|
|
16
19
|
event.preventDefault();
|
|
17
|
-
|
|
20
|
+
if (event.name === "m") {
|
|
21
|
+
props.onDismiss();
|
|
22
|
+
void spawnPager(buildManPage(), props.renderer);
|
|
23
|
+
} else {
|
|
24
|
+
props.onDismiss();
|
|
25
|
+
}
|
|
18
26
|
});
|
|
19
27
|
|
|
20
|
-
const commands = getAllCommands();
|
|
21
|
-
|
|
22
28
|
return (
|
|
23
29
|
<box
|
|
24
30
|
position="absolute"
|
|
@@ -31,43 +37,67 @@ export function HelpOverlay(props: HelpOverlayProps) {
|
|
|
31
37
|
justifyContent="center"
|
|
32
38
|
>
|
|
33
39
|
<box
|
|
34
|
-
width={
|
|
40
|
+
width={82}
|
|
35
41
|
backgroundColor="#1a1a2e"
|
|
36
42
|
borderStyle="single"
|
|
37
43
|
borderColor="#586e75"
|
|
38
44
|
padding={2}
|
|
39
45
|
flexDirection="column"
|
|
46
|
+
gap={1}
|
|
40
47
|
>
|
|
41
48
|
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
/
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
<box flexDirection="row" gap={2}>
|
|
50
|
+
|
|
51
|
+
<box flexDirection="column" gap={1} width={38}>
|
|
52
|
+
<box flexDirection="column">
|
|
53
|
+
<text fg="#eee8d5">Keybindings</text>
|
|
54
|
+
<text fg="#93a1a1"> Ctrl+E Open $EDITOR (preserves input)</text>
|
|
55
|
+
<text fg="#93a1a1"> Ctrl+C Clear input / exit</text>
|
|
56
|
+
<text fg="#93a1a1"> Ctrl+B Toggle sidebar</text>
|
|
57
|
+
<text fg="#93a1a1"> Escape Abort / resume queue</text>
|
|
58
|
+
<text fg="#93a1a1"> PgUp/Dn Scroll messages</text>
|
|
59
|
+
</box>
|
|
60
|
+
|
|
61
|
+
<box flexDirection="column">
|
|
62
|
+
<text fg="#eee8d5">Core</text>
|
|
63
|
+
<text fg="#93a1a1"> /set Edit global settings</text>
|
|
64
|
+
<text fg="#93a1a1"> /q /q! Quit (! = skip sync)</text>
|
|
65
|
+
<text fg="#93a1a1"> /provider Manage LLM providers</text>
|
|
66
|
+
<text fg="#93a1a1"> /me Edit your data</text>
|
|
67
|
+
<text fg="#93a1a1"> /d /d <name> Edit persona details</text>
|
|
68
|
+
</box>
|
|
69
|
+
</box>
|
|
70
|
+
|
|
71
|
+
<box flexDirection="column" gap={1} width={38}>
|
|
72
|
+
<box flexDirection="column">
|
|
73
|
+
<text fg="#eee8d5">Persona</text>
|
|
74
|
+
<text fg="#93a1a1"> /p /p new /p update</text>
|
|
75
|
+
<text fg="#93a1a1"> /context Message context</text>
|
|
76
|
+
<text fg="#93a1a1"> /pause /resume</text>
|
|
77
|
+
</box>
|
|
78
|
+
|
|
79
|
+
<box flexDirection="column">
|
|
80
|
+
<text fg="#eee8d5">Rooms</text>
|
|
81
|
+
<text fg="#93a1a1"> /r /r new Room picker / create</text>
|
|
82
|
+
<text fg="#93a1a1"> /activate Advance active node</text>
|
|
83
|
+
<text fg="#93a1a1"> /silence Pass your turn</text>
|
|
84
|
+
</box>
|
|
85
|
+
|
|
86
|
+
<box flexDirection="column">
|
|
87
|
+
<text fg="#eee8d5">Extended</text>
|
|
88
|
+
<text fg="#93a1a1"> /tools Tool providers</text>
|
|
89
|
+
<text fg="#93a1a1"> /auth spotify Spotify OAuth</text>
|
|
90
|
+
<text fg="#93a1a1"> /queue /dlq Inspect queues</text>
|
|
91
|
+
</box>
|
|
92
|
+
</box>
|
|
93
|
+
|
|
94
|
+
</box>
|
|
53
95
|
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
<text fg="#93a1a1">Ctrl+C - Clear input / Exit</text>
|
|
59
|
-
<text fg="#93a1a1">Ctrl+B - Toggle sidebar</text>
|
|
60
|
-
<text fg="#93a1a1">Ctrl+E - Edit in $EDITOR</text>
|
|
61
|
-
<text fg="#93a1a1">PageUp/Down - Scroll messages</text>
|
|
62
|
-
<text> </text>
|
|
96
|
+
<box flexDirection="column">
|
|
97
|
+
<text fg="#586e75"> m - full manual | any key - dismiss</text>
|
|
98
|
+
<text fg="#2a2a3e"> Ei - 永 (ei) - eternal</text>
|
|
99
|
+
</box>
|
|
63
100
|
|
|
64
|
-
<text fg="#586e75">
|
|
65
|
-
Press any key to dismiss
|
|
66
|
-
</text>
|
|
67
|
-
<text> </text>
|
|
68
|
-
<text fg="#2a2a3e">
|
|
69
|
-
Ei - 永 (ei) - eternal
|
|
70
|
-
</text>
|
|
71
101
|
</box>
|
|
72
102
|
</box>
|
|
73
103
|
);
|
|
@@ -33,8 +33,10 @@ export const OverlayProvider: ParentComponent = (props) => {
|
|
|
33
33
|
const hideOverlay = () => {
|
|
34
34
|
logger.debug("[overlay] hideOverlay called");
|
|
35
35
|
setOverlayRenderer(null);
|
|
36
|
+
cliRenderer?.requestRender();
|
|
36
37
|
};
|
|
37
38
|
setOverlayRenderer(() => () => renderer(hideOverlay, hideForEditor));
|
|
39
|
+
queueMicrotask(() => cliRenderer?.requestRender());
|
|
38
40
|
};
|
|
39
41
|
|
|
40
42
|
const hideOverlay = () => {
|
package/tui/src/index.tsx
CHANGED
|
@@ -4,6 +4,13 @@ import { App } from "./app";
|
|
|
4
4
|
|
|
5
5
|
import { InstanceLock } from "./util/instance-lock";
|
|
6
6
|
import { FileStorage } from "./storage/file";
|
|
7
|
+
import pkg from "../../package.json";
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
if (args.includes("--version") || args.includes("version") || args.includes("-v")) {
|
|
11
|
+
process.stdout.write(`${pkg.version}\n`);
|
|
12
|
+
process.exit(0);
|
|
13
|
+
}
|
|
7
14
|
|
|
8
15
|
const storage = new FileStorage(Bun.env.EI_DATA_PATH);
|
|
9
16
|
const lock = new InstanceLock(storage.getDataPath());
|
package/tui/src/util/editor.ts
CHANGED
|
@@ -23,6 +23,41 @@ export interface EditorResult {
|
|
|
23
23
|
aborted: boolean;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
export async function spawnPager(content: string, renderer: CliRenderer): Promise<void> {
|
|
27
|
+
const pager = process.env.PAGER || "less";
|
|
28
|
+
const tmpDir = os.tmpdir();
|
|
29
|
+
const tmpFile = path.join(tmpDir, `ei-help-${Date.now()}.txt`);
|
|
30
|
+
|
|
31
|
+
fs.writeFileSync(tmpFile, content, "utf-8");
|
|
32
|
+
|
|
33
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
34
|
+
|
|
35
|
+
return new Promise((resolve) => {
|
|
36
|
+
renderer.suspend();
|
|
37
|
+
renderer.currentRenderBuffer.clear();
|
|
38
|
+
|
|
39
|
+
const child = spawn(pager, [tmpFile], {
|
|
40
|
+
stdio: "inherit",
|
|
41
|
+
shell: true,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
child.on("error", () => {
|
|
45
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
46
|
+
renderer.resume();
|
|
47
|
+
renderer.requestRender();
|
|
48
|
+
resolve();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
child.on("exit", () => {
|
|
52
|
+
try { fs.unlinkSync(tmpFile); } catch {}
|
|
53
|
+
renderer.currentRenderBuffer.clear();
|
|
54
|
+
renderer.resume();
|
|
55
|
+
renderer.requestRender();
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
26
61
|
export async function spawnEditorRaw(options: EditorRawOptions): Promise<EditorResult> {
|
|
27
62
|
const { initialContent, filename } = options;
|
|
28
63
|
const editor = process.env.EDITOR || process.env.VISUAL || "vi";
|
|
@@ -152,9 +187,7 @@ export async function spawnEditor(options: EditorOptions): Promise<EditorResult>
|
|
|
152
187
|
logger.debug("[editor] already suspended before spawn, skipping resume");
|
|
153
188
|
}
|
|
154
189
|
|
|
155
|
-
|
|
156
|
-
renderer.requestRender();
|
|
157
|
-
});
|
|
190
|
+
renderer.requestRender();
|
|
158
191
|
|
|
159
192
|
if (code !== 0) {
|
|
160
193
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
export function buildManPage(): string {
|
|
2
|
+
return `EI(1) Ei Terminal UI EI(1)
|
|
3
|
+
|
|
4
|
+
NAME
|
|
5
|
+
ei - local-first AI companion with persistent personas
|
|
6
|
+
|
|
7
|
+
KEYBINDINGS
|
|
8
|
+
Ctrl+E Open current input in $EDITOR (preserves text)
|
|
9
|
+
Ctrl+C Clear input field (second press exits)
|
|
10
|
+
Ctrl+B Toggle sidebar
|
|
11
|
+
Escape Abort operation / resume queue
|
|
12
|
+
PageUp/Down Scroll message history
|
|
13
|
+
|
|
14
|
+
CORE COMMANDS
|
|
15
|
+
/settings, /set
|
|
16
|
+
Edit global settings in $EDITOR. Configure default model,
|
|
17
|
+
heartbeat interval, context window, integrations, and more.
|
|
18
|
+
|
|
19
|
+
/quit, /q
|
|
20
|
+
Save, sync, and exit. Add ! to force quit without syncing: /q!
|
|
21
|
+
|
|
22
|
+
/provider
|
|
23
|
+
Open provider picker to select, edit, or create LLM providers.
|
|
24
|
+
/provider <Name> Set provider on active persona
|
|
25
|
+
/provider <Name>:<Model> Set provider and model explicitly
|
|
26
|
+
/provider new Create a new provider
|
|
27
|
+
|
|
28
|
+
/me
|
|
29
|
+
Edit your personal data (facts, topics, people) in $EDITOR.
|
|
30
|
+
Each section includes a commented stub — uncomment and fill it in
|
|
31
|
+
to create a new entry. No UUID required; one is generated for you.
|
|
32
|
+
|
|
33
|
+
/me fact Edit only facts (stub included)
|
|
34
|
+
/me topic Edit only topics (stub included)
|
|
35
|
+
/me person Edit only people (stub included)
|
|
36
|
+
/me fact new Open with just the new-fact stub
|
|
37
|
+
/me fact coffee Filter facts whose name contains "coffee"
|
|
38
|
+
/me person "New York" Quoted search for multi-word names
|
|
39
|
+
|
|
40
|
+
/details, /d
|
|
41
|
+
Edit the current persona's details in $EDITOR.
|
|
42
|
+
/d <name> Edit a specific persona by name
|
|
43
|
+
|
|
44
|
+
PERSONA COMMANDS
|
|
45
|
+
/persona, /p
|
|
46
|
+
Open persona picker. Switch, list, or create personas.
|
|
47
|
+
/p new <name> Create a new persona
|
|
48
|
+
/p update <name> [person] Regenerate persona from a person record
|
|
49
|
+
|
|
50
|
+
/context, /messages
|
|
51
|
+
Edit which messages are included in LLM context.
|
|
52
|
+
|
|
53
|
+
/pause
|
|
54
|
+
Pause the current persona indefinitely.
|
|
55
|
+
/pause <duration> Pause for a duration: 2h, 1d, 1w
|
|
56
|
+
|
|
57
|
+
/resume, /unpause
|
|
58
|
+
Resume the current paused persona.
|
|
59
|
+
/resume <name> Resume a specific persona
|
|
60
|
+
|
|
61
|
+
/new
|
|
62
|
+
Toggle a context boundary — starts a fresh conversation thread
|
|
63
|
+
without deleting history.
|
|
64
|
+
|
|
65
|
+
/quotes, /quote
|
|
66
|
+
Manage quotes attached to messages.
|
|
67
|
+
/quotes <N> View quotes from message N
|
|
68
|
+
/quotes me View your own quotes
|
|
69
|
+
/quotes search "term" Search quotes by keyword
|
|
70
|
+
/quotes <persona> View a persona's quotes
|
|
71
|
+
|
|
72
|
+
ROOM COMMANDS
|
|
73
|
+
/room, /r
|
|
74
|
+
Open room picker. Switch or create rooms.
|
|
75
|
+
/room new Create a new room (FFA, CYP, or MAP mode)
|
|
76
|
+
/room new <name> Create with a pre-filled name
|
|
77
|
+
|
|
78
|
+
/activate, /a
|
|
79
|
+
Advance the active node in a CYP or MAP room.
|
|
80
|
+
/activate <num> Activate a specific response by number
|
|
81
|
+
|
|
82
|
+
/silence
|
|
83
|
+
Pass your turn in a room with an optional reason.
|
|
84
|
+
/silence [reason]
|
|
85
|
+
|
|
86
|
+
/capture
|
|
87
|
+
Force-extract quotes, topics, and people from the current chat now.
|
|
88
|
+
|
|
89
|
+
EXTENDED COMMANDS
|
|
90
|
+
/tools
|
|
91
|
+
Manage tool providers — enable or disable tools per persona.
|
|
92
|
+
|
|
93
|
+
/auth
|
|
94
|
+
Authenticate with an external service.
|
|
95
|
+
/auth spotify Connect your Spotify account
|
|
96
|
+
|
|
97
|
+
/queue
|
|
98
|
+
Pause the queue and inspect or edit active items in $EDITOR.
|
|
99
|
+
|
|
100
|
+
/dlq
|
|
101
|
+
Inspect and recover failed (dead-letter) queue items in $EDITOR.
|
|
102
|
+
|
|
103
|
+
/dedupe
|
|
104
|
+
Find and merge duplicate people or topics.
|
|
105
|
+
/dedupe person Flare "Jeremy Scherer"
|
|
106
|
+
/dedupe topic AI "artificial intelligence"
|
|
107
|
+
|
|
108
|
+
/archive
|
|
109
|
+
Archive a persona or room. Lists archived items if no name given.
|
|
110
|
+
/archive <name> Archive by name
|
|
111
|
+
|
|
112
|
+
/unarchive
|
|
113
|
+
Restore an archived persona or room and switch to it.
|
|
114
|
+
/unarchive <name>
|
|
115
|
+
|
|
116
|
+
/delete, /del
|
|
117
|
+
Permanently delete a persona. Cannot be undone.
|
|
118
|
+
/delete <name>
|
|
119
|
+
|
|
120
|
+
/setsync, /ss
|
|
121
|
+
Set sync credentials (triggers restart).
|
|
122
|
+
/setsync <username> <passphrase>
|
|
123
|
+
|
|
124
|
+
/editor, /e, /edit
|
|
125
|
+
Open $EDITOR with the current input field contents.
|
|
126
|
+
Note: Ctrl+E does the same thing without clearing the input first.
|
|
127
|
+
|
|
128
|
+
TIPS
|
|
129
|
+
- Append ! to any command as shorthand for --force: /quit!
|
|
130
|
+
- Duration strings: 30m, 2h, 1d, 1w (used by /pause, /settings)
|
|
131
|
+
- All editor fields that say "null" inherit from your global settings
|
|
132
|
+
- $EDITOR and $PAGER are respected throughout
|
|
133
|
+
|
|
134
|
+
Ei - 永 (ei) - eternal
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
@@ -51,6 +51,19 @@ function readOnlyToEnd<T extends WithReadOnlyFields>(item: T): T {
|
|
|
51
51
|
return { ...rest, learned_on, learned_by, validated_date, last_mentioned, last_updated, last_changed_by } as T;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
const FIELD_ORDER = ['id', 'name', 'description', 'sentiment', 'relationship', 'category', 'exposure_current', 'exposure_desired'];
|
|
55
|
+
|
|
56
|
+
function canonicalFieldOrder<T extends object>(item: T): T {
|
|
57
|
+
const ordered: Record<string, unknown> = {};
|
|
58
|
+
for (const key of FIELD_ORDER) {
|
|
59
|
+
if (key in item) ordered[key] = (item as Record<string, unknown>)[key];
|
|
60
|
+
}
|
|
61
|
+
for (const [key, val] of Object.entries(item)) {
|
|
62
|
+
if (!(key in ordered)) ordered[key] = val;
|
|
63
|
+
}
|
|
64
|
+
return ordered as T;
|
|
65
|
+
}
|
|
66
|
+
|
|
54
67
|
function buildGroupCheckboxMap(itemGroups: string[], allGroups: string[]): Record<string, boolean>[] {
|
|
55
68
|
const activeSet = new Set(itemGroups);
|
|
56
69
|
return [...new Set([...allGroups, ...itemGroups])].map(g => ({ [g]: activeSet.has(g) }));
|
|
@@ -65,8 +78,10 @@ function toYAMLIdentifiers(identifiers: PersonIdentifier[], personaLookup?: Map<
|
|
|
65
78
|
});
|
|
66
79
|
}
|
|
67
80
|
|
|
68
|
-
function knownTypesComment(personaLookup?: Map<string, string>): string {
|
|
69
|
-
const
|
|
81
|
+
function knownTypesComment(people: Person[], personaLookup?: Map<string, string>): string {
|
|
82
|
+
const userTypes = people.flatMap(p => (p.identifiers ?? []).map(i => i.type));
|
|
83
|
+
const allTypes = [...new Set([...BUILT_IN_IDENTIFIER_TYPES, ...userTypes])];
|
|
84
|
+
const lines = [`# Valid types: ${allTypes.join(', ')}`];
|
|
70
85
|
if (personaLookup && personaLookup.size > 0) {
|
|
71
86
|
lines.push(`# Personas: ${Array.from(personaLookup.values()).join(', ')}`);
|
|
72
87
|
}
|
|
@@ -84,11 +99,68 @@ function parseGroupCheckboxMap(groups: Record<string, boolean>[] | undefined): s
|
|
|
84
99
|
return result;
|
|
85
100
|
}
|
|
86
101
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
102
|
+
function sectionStub(type: "facts" | "topics" | "people", people: Person[], personaLookup?: Map<string, string>): string {
|
|
103
|
+
if (type === "facts") {
|
|
104
|
+
return [
|
|
105
|
+
` # --- New Fact (uncomment to create) ---`,
|
|
106
|
+
` # - name: ''`,
|
|
107
|
+
` # description: ''`,
|
|
108
|
+
` # sentiment: 0`,
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (type === "topics") {
|
|
113
|
+
return [
|
|
114
|
+
` # --- New Topic (uncomment to create) ---`,
|
|
115
|
+
` # - name: ''`,
|
|
116
|
+
` # description: ''`,
|
|
117
|
+
` # category: '' # Interest, Goal, Dream, Conflict, Concern, Fear, Hope, Plan, Project`,
|
|
118
|
+
` # exposure_desired: 0.5`,
|
|
119
|
+
` # sentiment: 0`,
|
|
120
|
+
].join('\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const userTypes = people.flatMap(p => (p.identifiers ?? []).map(i => i.type));
|
|
124
|
+
const allTypes = [...new Set([...BUILT_IN_IDENTIFIER_TYPES, ...userTypes])];
|
|
125
|
+
const identifierTypeHint = allTypes.join(', ');
|
|
126
|
+
const personaNames = personaLookup && personaLookup.size > 0
|
|
127
|
+
? Array.from(personaLookup.values()).join(', ')
|
|
128
|
+
: null;
|
|
129
|
+
|
|
130
|
+
return [
|
|
131
|
+
` # --- New Person (uncomment to create) ---`,
|
|
132
|
+
` # - name: ''`,
|
|
133
|
+
` # description: ''`,
|
|
134
|
+
` # relationship: ''`,
|
|
135
|
+
` # exposure_desired: 0.5`,
|
|
136
|
+
` # sentiment: 0`,
|
|
137
|
+
` # identifiers:`,
|
|
138
|
+
` # # Valid types: ${identifierTypeHint}`,
|
|
139
|
+
...(personaNames ? [` # # Personas: ${personaNames}`] : []),
|
|
140
|
+
` # - type: ''`,
|
|
141
|
+
` # value: ''`,
|
|
142
|
+
` # primary: true`,
|
|
143
|
+
].join('\n');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function humanToYAML(
|
|
147
|
+
human: HumanEntity,
|
|
148
|
+
personaLookup?: Map<string, string>,
|
|
149
|
+
allGroups: string[] = [],
|
|
150
|
+
sections?: Set<"facts" | "topics" | "people">,
|
|
151
|
+
): string {
|
|
152
|
+
const activeSections = sections ?? new Set<"facts" | "topics" | "people">(["facts", "topics", "people"]);
|
|
153
|
+
|
|
154
|
+
const data: Partial<EditableHumanData> = {};
|
|
155
|
+
|
|
156
|
+
if (activeSections.has("facts") && human.facts.length > 0) {
|
|
157
|
+
data.facts = human.facts.map(f => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(f); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; });
|
|
158
|
+
}
|
|
159
|
+
if (activeSections.has("topics") && human.topics.length > 0) {
|
|
160
|
+
data.topics = human.topics.map(t => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(t); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; });
|
|
161
|
+
}
|
|
162
|
+
if (activeSections.has("people") && human.people.length > 0) {
|
|
163
|
+
data.people = human.people.map(p => {
|
|
92
164
|
const { identifiers, interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(p);
|
|
93
165
|
return {
|
|
94
166
|
...rest,
|
|
@@ -96,31 +168,51 @@ export function humanToYAML(human: HumanEntity, personaLookup?: Map<string, stri
|
|
|
96
168
|
identifiers: toYAMLIdentifiers(identifiers ?? [], personaLookup),
|
|
97
169
|
_delete: false as const,
|
|
98
170
|
};
|
|
99
|
-
})
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const personComment = knownTypesComment(human.people, personaLookup);
|
|
175
|
+
|
|
176
|
+
const applyReadOnlyMarkers = (yaml: string): string =>
|
|
177
|
+
yaml
|
|
178
|
+
.replace(/^(\s+)(learned_on: .+)$/mg, '$1# [read-only] $2')
|
|
179
|
+
.replace(/^(\s+)(learned_by: )(.+)$/mg, (_, indent, key, val) => {
|
|
180
|
+
const trimmed = val.trim();
|
|
181
|
+
const displayName = personaLookup?.get(trimmed) ?? trimmed;
|
|
182
|
+
return `${indent}# [read-only] ${key}${displayName}`;
|
|
183
|
+
})
|
|
184
|
+
.replace(/^(\s+)(validated_date: .+)$/mg, '$1# [read-only] $2')
|
|
185
|
+
.replace(/^(\s+)(last_mentioned: .+)$/mg, '$1# [read-only] $2')
|
|
186
|
+
.replace(/^(\s+)(last_updated: .+)$/mg, '$1# [read-only] $2')
|
|
187
|
+
.replace(/^(\s+)(last_changed_by: )(.+)$/mg, (_, indent, key, val) => {
|
|
188
|
+
const trimmed = val.trim();
|
|
189
|
+
const displayName = personaLookup?.get(trimmed) ?? trimmed;
|
|
190
|
+
return `${indent}# [read-only] ${key}${displayName}`;
|
|
191
|
+
})
|
|
192
|
+
.replace(/^(\s+)(identifiers:)/mg, (_, indent, _key) => {
|
|
193
|
+
return `${indent}${personComment}\n${indent}identifiers:`;
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
const serializeSection = (key: "facts" | "topics" | "people", items: unknown[] | undefined): string => {
|
|
197
|
+
const stub = sectionStub(key, human.people, personaLookup);
|
|
198
|
+
if (!items || items.length === 0) {
|
|
199
|
+
return `${key}:\n${stub}`;
|
|
200
|
+
}
|
|
201
|
+
const ordered = (items as object[]).map(canonicalFieldOrder);
|
|
202
|
+
const itemsYaml = YAML.stringify(ordered, { lineWidth: 0 })
|
|
203
|
+
.split('\n')
|
|
204
|
+
.map(line => ` ${line}`)
|
|
205
|
+
.join('\n')
|
|
206
|
+
.trimEnd();
|
|
207
|
+
return `${key}:\n${applyReadOnlyMarkers(itemsYaml)}\n${stub}`;
|
|
100
208
|
};
|
|
101
209
|
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
108
|
-
.replace(/^(\s+)(learned_by: )(.+)$/mg, (_, indent, key, val) => {
|
|
109
|
-
const trimmed = val.trim();
|
|
110
|
-
const displayName = personaLookup?.get(trimmed) ?? trimmed;
|
|
111
|
-
return `${indent}# [read-only] ${key}${displayName}`;
|
|
112
|
-
})
|
|
113
|
-
.replace(/^(\s+)(validated_date: .+)$/mg, '$1# [read-only] $2')
|
|
114
|
-
.replace(/^(\s+)(last_mentioned: .+)$/mg, '$1# [read-only] $2')
|
|
115
|
-
.replace(/^(\s+)(last_updated: .+)$/mg, '$1# [read-only] $2')
|
|
116
|
-
.replace(/^(\s+)(last_changed_by: )(.+)$/mg, (_, indent, key, val) => {
|
|
117
|
-
const trimmed = val.trim();
|
|
118
|
-
const displayName = personaLookup?.get(trimmed) ?? trimmed;
|
|
119
|
-
return `${indent}# [read-only] ${key}${displayName}`;
|
|
120
|
-
})
|
|
121
|
-
.replace(/^(\s+)(identifiers:)/mg, (_, indent, _key) => {
|
|
122
|
-
return `${indent}${personComment}\n${indent}identifiers:`;
|
|
123
|
-
});
|
|
210
|
+
const parts: string[] = [];
|
|
211
|
+
if (activeSections.has("facts")) parts.push(serializeSection("facts", data.facts));
|
|
212
|
+
if (activeSections.has("topics")) parts.push(serializeSection("topics", data.topics));
|
|
213
|
+
if (activeSections.has("people")) parts.push(serializeSection("people", data.people));
|
|
214
|
+
|
|
215
|
+
return parts.join('\n') + '\n';
|
|
124
216
|
}
|
|
125
217
|
|
|
126
218
|
export interface HumanYAMLResult {
|
|
@@ -192,7 +284,7 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
192
284
|
.split('\n')
|
|
193
285
|
.filter(line => !/^\s*#\s*\[read-only\]/.test(line))
|
|
194
286
|
.join('\n');
|
|
195
|
-
const data = YAML.parse(stripped) as EditableHumanData;
|
|
287
|
+
const data = (YAML.parse(stripped) ?? {}) as EditableHumanData;
|
|
196
288
|
|
|
197
289
|
const deletedFactIds: string[] = [];
|
|
198
290
|
const deletedTopicIds: string[] = [];
|
|
@@ -207,10 +299,11 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
207
299
|
deletedFactIds.push(f.id);
|
|
208
300
|
} else {
|
|
209
301
|
const { _delete, persona_groups: groupMap, ...parsed } = f;
|
|
302
|
+
if (!parsed.id) parsed.id = crypto.randomUUID();
|
|
210
303
|
const originalFact = original?.facts.find(of => of.id === parsed.id);
|
|
211
304
|
const fact: Fact = originalFact
|
|
212
305
|
? { ...originalFact, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
213
|
-
: { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
306
|
+
: { ...parsed, last_updated: new Date().toISOString(), persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
214
307
|
facts.push(fact);
|
|
215
308
|
if (!originalFact || factChanged(fact, originalFact)) {
|
|
216
309
|
if (fact.description && !originalFact?.validated_date) {
|
|
@@ -227,10 +320,11 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
227
320
|
deletedTopicIds.push(t.id);
|
|
228
321
|
} else {
|
|
229
322
|
const { _delete, persona_groups: groupMap, ...parsed } = t;
|
|
323
|
+
if (!parsed.id) parsed.id = crypto.randomUUID();
|
|
230
324
|
const originalTopic = original?.topics.find(ot => ot.id === parsed.id);
|
|
231
325
|
const topic: Topic = originalTopic
|
|
232
326
|
? { ...originalTopic, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
233
|
-
: { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
327
|
+
: { ...parsed, last_updated: new Date().toISOString(), persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
234
328
|
topics.push(topic);
|
|
235
329
|
if (!originalTopic || topicChanged(topic, originalTopic)) {
|
|
236
330
|
changedTopicIds.add(topic.id);
|
|
@@ -244,6 +338,7 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
244
338
|
deletedPersonIds.push(p.id);
|
|
245
339
|
} else {
|
|
246
340
|
const { _delete, identifiers: yamlIdentifiers, persona_groups: groupMap, ...parsed } = p;
|
|
341
|
+
if (!parsed.id) parsed.id = crypto.randomUUID();
|
|
247
342
|
const identifiers: PersonIdentifier[] = (yamlIdentifiers ?? []).map(({ type, value, primary }) => ({
|
|
248
343
|
type,
|
|
249
344
|
value,
|
|
@@ -252,7 +347,7 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
252
347
|
const originalPerson = original?.people.find(op => op.id === parsed.id);
|
|
253
348
|
const person: Person = originalPerson
|
|
254
349
|
? { ...originalPerson, ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
255
|
-
: { ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
350
|
+
: { ...parsed, last_updated: new Date().toISOString(), identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
256
351
|
people.push(person);
|
|
257
352
|
if (!originalPerson || personChanged(person, originalPerson)) {
|
|
258
353
|
changedPersonIds.add(person.id);
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ProviderAccount,
|
|
8
8
|
} from "../../../src/core/types.js";
|
|
9
9
|
import { modelGuidToDisplay, displayToModelGuid } from "./yaml-shared.js";
|
|
10
|
+
import { parseDuration, formatDuration } from "./duration.js";
|
|
10
11
|
|
|
11
12
|
const PLACEHOLDER_LONG_DESC = "Detailed description of this persona's personality, background, and role";
|
|
12
13
|
|
|
@@ -36,8 +37,8 @@ interface EditablePersonaData {
|
|
|
36
37
|
groups_visible?: Record<string, boolean>[];
|
|
37
38
|
traits: YAMLTrait[];
|
|
38
39
|
topics: YAMLPersonaTopic[];
|
|
39
|
-
heartbeat_delay_ms?: string |
|
|
40
|
-
context_window_hours?: number;
|
|
40
|
+
heartbeat_delay_ms?: string | null;
|
|
41
|
+
context_window_hours?: number | null;
|
|
41
42
|
is_paused?: boolean;
|
|
42
43
|
pause_until?: string;
|
|
43
44
|
is_static?: boolean;
|
|
@@ -175,10 +176,10 @@ export function newPersonaFromYAML(yamlContent: string, allTools?: ToolDefinitio
|
|
|
175
176
|
groups_visible: groupsVisible.length > 0 ? groupsVisible : ["General"],
|
|
176
177
|
traits,
|
|
177
178
|
topics,
|
|
178
|
-
heartbeat_delay_ms: data.heartbeat_delay_ms
|
|
179
|
+
heartbeat_delay_ms: data.heartbeat_delay_ms == null
|
|
179
180
|
? undefined
|
|
180
|
-
:
|
|
181
|
-
context_window_hours: data.context_window_hours,
|
|
181
|
+
: parseDuration(data.heartbeat_delay_ms) ?? undefined,
|
|
182
|
+
context_window_hours: data.context_window_hours ?? undefined,
|
|
182
183
|
tools: resolvePersonaToolsFromMap(data.tools, allTools ?? [], allProviders ?? []),
|
|
183
184
|
};
|
|
184
185
|
}
|
|
@@ -216,8 +217,8 @@ export function personaToYAML(persona: PersonaEntity, allGroups?: string[], allT
|
|
|
216
217
|
: persona.topics.map(({ name, perspective, approach, personal_stake, exposure_current, exposure_desired }) => ({
|
|
217
218
|
name, perspective, approach, personal_stake, exposure_current, exposure_desired
|
|
218
219
|
})),
|
|
219
|
-
heartbeat_delay_ms: persona.heartbeat_delay_ms
|
|
220
|
-
context_window_hours: persona.context_window_hours,
|
|
220
|
+
heartbeat_delay_ms: persona.heartbeat_delay_ms ? formatDuration(persona.heartbeat_delay_ms) : null,
|
|
221
|
+
context_window_hours: persona.context_window_hours ?? null,
|
|
221
222
|
is_paused: persona.is_paused || undefined,
|
|
222
223
|
pause_until: persona.pause_until,
|
|
223
224
|
is_static: persona.is_static || undefined,
|
|
@@ -324,10 +325,10 @@ export function personaFromYAML(yamlContent: string, original: PersonaEntity, al
|
|
|
324
325
|
groups_visible: groupsVisible,
|
|
325
326
|
traits,
|
|
326
327
|
topics,
|
|
327
|
-
heartbeat_delay_ms: data.heartbeat_delay_ms
|
|
328
|
+
heartbeat_delay_ms: data.heartbeat_delay_ms == null
|
|
328
329
|
? undefined
|
|
329
|
-
:
|
|
330
|
-
context_window_hours: data.context_window_hours,
|
|
330
|
+
: parseDuration(data.heartbeat_delay_ms) ?? undefined,
|
|
331
|
+
context_window_hours: data.context_window_hours ?? undefined,
|
|
331
332
|
is_paused: data.is_paused ?? false,
|
|
332
333
|
pause_until: data.pause_until,
|
|
333
334
|
is_static: data.is_static ?? false,
|