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,755 @@
|
|
|
1
|
+
import YAML from "yaml";
|
|
2
|
+
import type {
|
|
3
|
+
PersonaEntity,
|
|
4
|
+
HumanEntity,
|
|
5
|
+
HumanSettings,
|
|
6
|
+
CeremonyConfig,
|
|
7
|
+
OpenCodeSettings,
|
|
8
|
+
Fact,
|
|
9
|
+
Trait,
|
|
10
|
+
Topic,
|
|
11
|
+
Person,
|
|
12
|
+
PersonaTopic,
|
|
13
|
+
ProviderAccount,
|
|
14
|
+
ProviderType,
|
|
15
|
+
Quote,
|
|
16
|
+
Message,
|
|
17
|
+
} from "../../../src/core/types.js";
|
|
18
|
+
import { ContextStatus } from "../../../src/core/types.js";
|
|
19
|
+
|
|
20
|
+
// =============================================================================
|
|
21
|
+
// TYPES FOR YAML EDITING
|
|
22
|
+
// =============================================================================
|
|
23
|
+
|
|
24
|
+
interface EditableTrait extends Trait {
|
|
25
|
+
_delete?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface EditableTopic extends Topic {
|
|
29
|
+
_delete?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface EditableFact extends Fact {
|
|
33
|
+
_delete?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface EditablePerson extends Person {
|
|
37
|
+
_delete?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface EditablePersonaData {
|
|
41
|
+
display_name?: string;
|
|
42
|
+
aliases?: string[];
|
|
43
|
+
short_description?: string;
|
|
44
|
+
long_description?: string;
|
|
45
|
+
model?: string;
|
|
46
|
+
group_primary?: string | null;
|
|
47
|
+
groups_visible?: Record<string, boolean>[];
|
|
48
|
+
traits: YAMLTrait[];
|
|
49
|
+
topics: YAMLPersonaTopic[];
|
|
50
|
+
heartbeat_delay_ms?: number;
|
|
51
|
+
context_window_hours?: number;
|
|
52
|
+
is_paused?: boolean;
|
|
53
|
+
pause_until?: string;
|
|
54
|
+
is_static?: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface EditableHumanData {
|
|
58
|
+
facts: EditableFact[];
|
|
59
|
+
traits: EditableTrait[];
|
|
60
|
+
topics: EditableTopic[];
|
|
61
|
+
people: EditablePerson[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// =============================================================================
|
|
65
|
+
// PLACEHOLDER MARKERS (stripped on parse if unchanged)
|
|
66
|
+
// =============================================================================
|
|
67
|
+
|
|
68
|
+
const PLACEHOLDER_LONG_DESC = "Detailed description of this persona's personality, background, and role";
|
|
69
|
+
|
|
70
|
+
// Placeholder types without id/_delete - these are for YAML display only
|
|
71
|
+
interface YAMLTrait {
|
|
72
|
+
name: string;
|
|
73
|
+
description: string;
|
|
74
|
+
strength: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface YAMLPersonaTopic {
|
|
78
|
+
name: string;
|
|
79
|
+
perspective: string;
|
|
80
|
+
approach: string;
|
|
81
|
+
personal_stake: string;
|
|
82
|
+
exposure_current: number;
|
|
83
|
+
exposure_desired: number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const PLACEHOLDER_TRAIT: YAMLTrait = {
|
|
87
|
+
name: "Example Trait",
|
|
88
|
+
description: "Delete this placeholder or modify it to define a real trait",
|
|
89
|
+
strength: 0.5,
|
|
90
|
+
};
|
|
91
|
+
const PLACEHOLDER_TOPIC: YAMLPersonaTopic = {
|
|
92
|
+
name: "Example Topic",
|
|
93
|
+
perspective: "How this persona views or thinks about this topic",
|
|
94
|
+
approach: "How this persona prefers to engage with this topic",
|
|
95
|
+
personal_stake: "Why this topic matters to this persona personally",
|
|
96
|
+
exposure_current: 0.5,
|
|
97
|
+
exposure_desired: 0.5,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// =============================================================================
|
|
101
|
+
// PERSONA SERIALIZATION
|
|
102
|
+
// =============================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Generate YAML skeleton for a NEW persona (doesn't exist yet)
|
|
106
|
+
*/
|
|
107
|
+
export function newPersonaToYAML(name: string): string {
|
|
108
|
+
const data: EditablePersonaData = {
|
|
109
|
+
display_name: name,
|
|
110
|
+
long_description: PLACEHOLDER_LONG_DESC,
|
|
111
|
+
model: undefined,
|
|
112
|
+
group_primary: "General",
|
|
113
|
+
groups_visible: [{ General: true }],
|
|
114
|
+
traits: [PLACEHOLDER_TRAIT],
|
|
115
|
+
topics: [PLACEHOLDER_TOPIC],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
return YAML.stringify(data, {
|
|
119
|
+
lineWidth: 0,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Parse YAML for a NEW persona (creates PersonaEntity from scratch)
|
|
125
|
+
*/
|
|
126
|
+
export function newPersonaFromYAML(yamlContent: string): Partial<PersonaEntity> {
|
|
127
|
+
const data = YAML.parse(yamlContent) as EditablePersonaData;
|
|
128
|
+
|
|
129
|
+
const isTraitPlaceholder = (t: YAMLTrait) =>
|
|
130
|
+
t.name === PLACEHOLDER_TRAIT.name &&
|
|
131
|
+
t.description === PLACEHOLDER_TRAIT.description;
|
|
132
|
+
|
|
133
|
+
const traits: Trait[] = [];
|
|
134
|
+
for (const t of data.traits ?? []) {
|
|
135
|
+
if (isTraitPlaceholder(t)) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
traits.push({
|
|
139
|
+
id: crypto.randomUUID(),
|
|
140
|
+
name: t.name,
|
|
141
|
+
description: t.description,
|
|
142
|
+
strength: t.strength,
|
|
143
|
+
sentiment: 0,
|
|
144
|
+
last_updated: new Date().toISOString(),
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const isTopicPlaceholder = (t: YAMLPersonaTopic) =>
|
|
149
|
+
t.name === PLACEHOLDER_TOPIC.name &&
|
|
150
|
+
t.perspective === PLACEHOLDER_TOPIC.perspective;
|
|
151
|
+
|
|
152
|
+
const topics: PersonaTopic[] = [];
|
|
153
|
+
for (const t of data.topics ?? []) {
|
|
154
|
+
if (isTopicPlaceholder(t)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
topics.push({
|
|
158
|
+
id: crypto.randomUUID(),
|
|
159
|
+
name: t.name,
|
|
160
|
+
perspective: t.perspective,
|
|
161
|
+
approach: t.approach,
|
|
162
|
+
personal_stake: t.personal_stake,
|
|
163
|
+
sentiment: 0,
|
|
164
|
+
exposure_current: t.exposure_current,
|
|
165
|
+
exposure_desired: t.exposure_desired,
|
|
166
|
+
last_updated: new Date().toISOString(),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const stripPlaceholder = (value: string | undefined, placeholder: string): string | undefined => {
|
|
171
|
+
return value === placeholder ? undefined : value;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
// Convert Record<string, boolean>[] to string[] - only include groups with true value
|
|
175
|
+
const groupsVisible: string[] = [];
|
|
176
|
+
for (const groupRecord of data.groups_visible ?? []) {
|
|
177
|
+
for (const [groupName, isVisible] of Object.entries(groupRecord)) {
|
|
178
|
+
if (isVisible) {
|
|
179
|
+
groupsVisible.push(groupName);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
long_description: stripPlaceholder(data.long_description, PLACEHOLDER_LONG_DESC),
|
|
186
|
+
model: data.model,
|
|
187
|
+
group_primary: data.group_primary ?? "General",
|
|
188
|
+
groups_visible: groupsVisible.length > 0 ? groupsVisible : ["General"],
|
|
189
|
+
traits,
|
|
190
|
+
topics,
|
|
191
|
+
heartbeat_delay_ms: data.heartbeat_delay_ms,
|
|
192
|
+
context_window_hours: data.context_window_hours,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function personaToYAML(persona: PersonaEntity, allGroups?: string[]): string {
|
|
197
|
+
const useTraitPlaceholder = persona.traits.length === 0;
|
|
198
|
+
const useTopicPlaceholder = persona.topics.length === 0;
|
|
199
|
+
|
|
200
|
+
const groupsForYAML: Record<string, boolean>[] = [];
|
|
201
|
+
const visibleSet = new Set(persona.groups_visible ?? []);
|
|
202
|
+
const groupsToShow = allGroups ?? persona.groups_visible ?? [];
|
|
203
|
+
for (const groupName of groupsToShow) {
|
|
204
|
+
groupsForYAML.push({ [groupName]: visibleSet.has(groupName) });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const data: EditablePersonaData = {
|
|
208
|
+
display_name: persona.display_name,
|
|
209
|
+
aliases: persona.aliases,
|
|
210
|
+
short_description: persona.short_description,
|
|
211
|
+
long_description: persona.long_description || PLACEHOLDER_LONG_DESC,
|
|
212
|
+
model: persona.model,
|
|
213
|
+
group_primary: persona.group_primary,
|
|
214
|
+
groups_visible: groupsForYAML,
|
|
215
|
+
traits: useTraitPlaceholder
|
|
216
|
+
? [PLACEHOLDER_TRAIT]
|
|
217
|
+
: persona.traits.map(({ name, description, strength }) => ({ name, description, strength: strength ?? 0.5 })),
|
|
218
|
+
topics: useTopicPlaceholder
|
|
219
|
+
? [PLACEHOLDER_TOPIC]
|
|
220
|
+
: persona.topics.map(({ name, perspective, approach, personal_stake, exposure_current, exposure_desired }) => ({
|
|
221
|
+
name, perspective, approach, personal_stake, exposure_current, exposure_desired
|
|
222
|
+
})),
|
|
223
|
+
heartbeat_delay_ms: persona.heartbeat_delay_ms,
|
|
224
|
+
context_window_hours: persona.context_window_hours,
|
|
225
|
+
is_paused: persona.is_paused || undefined,
|
|
226
|
+
pause_until: persona.pause_until,
|
|
227
|
+
is_static: persona.is_static || undefined,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
return YAML.stringify(data, {
|
|
231
|
+
lineWidth: 0,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface PersonaYAMLResult {
|
|
236
|
+
updates: Partial<PersonaEntity>;
|
|
237
|
+
deletedTraitIds: string[];
|
|
238
|
+
deletedTopicIds: string[];
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function personaFromYAML(yamlContent: string, original: PersonaEntity): PersonaYAMLResult {
|
|
242
|
+
const data = YAML.parse(yamlContent) as EditablePersonaData;
|
|
243
|
+
|
|
244
|
+
const deletedTraitIds: string[] = [];
|
|
245
|
+
const deletedTopicIds: string[] = [];
|
|
246
|
+
|
|
247
|
+
const isTraitPlaceholder = (t: YAMLTrait) =>
|
|
248
|
+
t.name === PLACEHOLDER_TRAIT.name &&
|
|
249
|
+
t.description === PLACEHOLDER_TRAIT.description;
|
|
250
|
+
|
|
251
|
+
const traits: Trait[] = [];
|
|
252
|
+
for (const t of data.traits ?? []) {
|
|
253
|
+
if (isTraitPlaceholder(t)) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
const existing = original.traits.find(orig => orig.name === t.name);
|
|
257
|
+
traits.push({
|
|
258
|
+
id: existing?.id ?? crypto.randomUUID(),
|
|
259
|
+
name: t.name,
|
|
260
|
+
description: t.description,
|
|
261
|
+
strength: t.strength,
|
|
262
|
+
sentiment: existing?.sentiment ?? 0,
|
|
263
|
+
last_updated: new Date().toISOString(),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const orig of original.traits) {
|
|
268
|
+
if (!traits.some(t => t.id === orig.id)) {
|
|
269
|
+
deletedTraitIds.push(orig.id);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const isTopicPlaceholder = (t: YAMLPersonaTopic) =>
|
|
274
|
+
t.name === PLACEHOLDER_TOPIC.name &&
|
|
275
|
+
t.perspective === PLACEHOLDER_TOPIC.perspective;
|
|
276
|
+
|
|
277
|
+
const topics: PersonaTopic[] = [];
|
|
278
|
+
for (const t of data.topics ?? []) {
|
|
279
|
+
if (isTopicPlaceholder(t)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
const existing = original.topics.find(orig => orig.name === t.name);
|
|
283
|
+
topics.push({
|
|
284
|
+
id: existing?.id ?? crypto.randomUUID(),
|
|
285
|
+
name: t.name,
|
|
286
|
+
perspective: t.perspective,
|
|
287
|
+
approach: t.approach,
|
|
288
|
+
personal_stake: t.personal_stake,
|
|
289
|
+
sentiment: existing?.sentiment ?? 0,
|
|
290
|
+
exposure_current: t.exposure_current,
|
|
291
|
+
exposure_desired: t.exposure_desired,
|
|
292
|
+
last_updated: new Date().toISOString(),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
for (const orig of original.topics) {
|
|
297
|
+
if (!topics.some(t => t.id === orig.id)) {
|
|
298
|
+
deletedTopicIds.push(orig.id);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const stripPlaceholder = (value: string | undefined, placeholder: string): string | undefined => {
|
|
303
|
+
return value === placeholder ? undefined : value;
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const groupsVisible: string[] = [];
|
|
307
|
+
for (const groupRecord of data.groups_visible ?? []) {
|
|
308
|
+
for (const [groupName, isVisible] of Object.entries(groupRecord)) {
|
|
309
|
+
if (isVisible) {
|
|
310
|
+
groupsVisible.push(groupName);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const updates: Partial<PersonaEntity> = {
|
|
316
|
+
display_name: data.display_name,
|
|
317
|
+
aliases: data.aliases,
|
|
318
|
+
short_description: data.short_description,
|
|
319
|
+
long_description: stripPlaceholder(data.long_description, PLACEHOLDER_LONG_DESC),
|
|
320
|
+
model: data.model,
|
|
321
|
+
group_primary: data.group_primary,
|
|
322
|
+
groups_visible: groupsVisible,
|
|
323
|
+
traits,
|
|
324
|
+
topics,
|
|
325
|
+
heartbeat_delay_ms: data.heartbeat_delay_ms,
|
|
326
|
+
context_window_hours: data.context_window_hours,
|
|
327
|
+
is_paused: data.is_paused ?? false,
|
|
328
|
+
pause_until: data.pause_until,
|
|
329
|
+
is_static: data.is_static ?? false,
|
|
330
|
+
last_updated: new Date().toISOString(),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
return { updates, deletedTraitIds, deletedTopicIds };
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// =============================================================================
|
|
337
|
+
// HUMAN SERIALIZATION
|
|
338
|
+
// =============================================================================
|
|
339
|
+
|
|
340
|
+
export function humanToYAML(human: HumanEntity): string {
|
|
341
|
+
const data: EditableHumanData = {
|
|
342
|
+
facts: human.facts.map(f => ({ ...f, _delete: false })),
|
|
343
|
+
traits: human.traits.map(t => ({ ...t, _delete: false })),
|
|
344
|
+
topics: human.topics.map(t => ({ ...t, _delete: false })),
|
|
345
|
+
people: human.people.map(p => ({ ...p, _delete: false })),
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
return YAML.stringify(data, {
|
|
349
|
+
lineWidth: 0,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export interface HumanYAMLResult {
|
|
354
|
+
facts: Fact[];
|
|
355
|
+
traits: Trait[];
|
|
356
|
+
topics: Topic[];
|
|
357
|
+
people: Person[];
|
|
358
|
+
deletedFactIds: string[];
|
|
359
|
+
deletedTraitIds: string[];
|
|
360
|
+
deletedTopicIds: string[];
|
|
361
|
+
deletedPersonIds: string[];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export function humanFromYAML(yamlContent: string): HumanYAMLResult {
|
|
365
|
+
const data = YAML.parse(yamlContent) as EditableHumanData;
|
|
366
|
+
|
|
367
|
+
const deletedFactIds: string[] = [];
|
|
368
|
+
const deletedTraitIds: string[] = [];
|
|
369
|
+
const deletedTopicIds: string[] = [];
|
|
370
|
+
const deletedPersonIds: string[] = [];
|
|
371
|
+
|
|
372
|
+
const facts: Fact[] = [];
|
|
373
|
+
for (const f of data.facts ?? []) {
|
|
374
|
+
if (f._delete) {
|
|
375
|
+
deletedFactIds.push(f.id);
|
|
376
|
+
} else {
|
|
377
|
+
const { _delete, ...fact } = f;
|
|
378
|
+
facts.push(fact);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const traits: Trait[] = [];
|
|
383
|
+
for (const t of data.traits ?? []) {
|
|
384
|
+
if (t._delete) {
|
|
385
|
+
deletedTraitIds.push(t.id);
|
|
386
|
+
} else {
|
|
387
|
+
const { _delete, ...trait } = t;
|
|
388
|
+
traits.push(trait);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const topics: Topic[] = [];
|
|
393
|
+
for (const t of data.topics ?? []) {
|
|
394
|
+
if (t._delete) {
|
|
395
|
+
deletedTopicIds.push(t.id);
|
|
396
|
+
} else {
|
|
397
|
+
const { _delete, ...topic } = t;
|
|
398
|
+
topics.push(topic);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const people: Person[] = [];
|
|
403
|
+
for (const p of data.people ?? []) {
|
|
404
|
+
if (p._delete) {
|
|
405
|
+
deletedPersonIds.push(p.id);
|
|
406
|
+
} else {
|
|
407
|
+
const { _delete, ...person } = p;
|
|
408
|
+
people.push(person);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
facts,
|
|
414
|
+
traits,
|
|
415
|
+
topics,
|
|
416
|
+
people,
|
|
417
|
+
deletedFactIds,
|
|
418
|
+
deletedTraitIds,
|
|
419
|
+
deletedTopicIds,
|
|
420
|
+
deletedPersonIds,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// =============================================================================
|
|
425
|
+
// SETTINGS SERIALIZATION
|
|
426
|
+
// =============================================================================
|
|
427
|
+
|
|
428
|
+
interface EditableSettingsData {
|
|
429
|
+
default_model?: string | null;
|
|
430
|
+
time_mode?: "24h" | "12h" | "local" | "utc" | null;
|
|
431
|
+
name_display?: string | null;
|
|
432
|
+
ceremony?: {
|
|
433
|
+
time: string;
|
|
434
|
+
decay_rate?: number | null;
|
|
435
|
+
explore_threshold?: number | null;
|
|
436
|
+
};
|
|
437
|
+
opencode?: {
|
|
438
|
+
integration?: boolean | null;
|
|
439
|
+
polling_interval_ms?: number | null;
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function settingsToYAML(settings: HumanSettings | undefined): string {
|
|
444
|
+
// Always show all editable fields, using null for unset values so YAML displays them
|
|
445
|
+
const data: EditableSettingsData = {
|
|
446
|
+
default_model: settings?.default_model ?? null,
|
|
447
|
+
time_mode: settings?.time_mode ?? null,
|
|
448
|
+
name_display: settings?.name_display ?? null,
|
|
449
|
+
ceremony: {
|
|
450
|
+
time: settings?.ceremony?.time ?? "09:00",
|
|
451
|
+
decay_rate: settings?.ceremony?.decay_rate ?? null,
|
|
452
|
+
explore_threshold: settings?.ceremony?.explore_threshold ?? null,
|
|
453
|
+
},
|
|
454
|
+
opencode: {
|
|
455
|
+
integration: settings?.opencode?.integration ?? null,
|
|
456
|
+
polling_interval_ms: settings?.opencode?.polling_interval_ms ?? null,
|
|
457
|
+
},
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
return YAML.stringify(data, {
|
|
461
|
+
lineWidth: 0,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export function settingsFromYAML(yamlContent: string, original: HumanSettings | undefined): HumanSettings {
|
|
466
|
+
const data = YAML.parse(yamlContent) as EditableSettingsData;
|
|
467
|
+
|
|
468
|
+
const nullToUndefined = <T>(value: T | null | undefined): T | undefined =>
|
|
469
|
+
value === null ? undefined : value;
|
|
470
|
+
|
|
471
|
+
let ceremony: CeremonyConfig | undefined;
|
|
472
|
+
if (data.ceremony) {
|
|
473
|
+
ceremony = {
|
|
474
|
+
time: data.ceremony.time,
|
|
475
|
+
decay_rate: nullToUndefined(data.ceremony.decay_rate),
|
|
476
|
+
explore_threshold: nullToUndefined(data.ceremony.explore_threshold),
|
|
477
|
+
last_ceremony: original?.ceremony?.last_ceremony,
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
let opencode: OpenCodeSettings | undefined;
|
|
482
|
+
if (data.opencode) {
|
|
483
|
+
opencode = {
|
|
484
|
+
integration: nullToUndefined(data.opencode.integration),
|
|
485
|
+
polling_interval_ms: nullToUndefined(data.opencode.polling_interval_ms),
|
|
486
|
+
last_sync: original?.opencode?.last_sync,
|
|
487
|
+
extraction_point: original?.opencode?.extraction_point,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
return {
|
|
492
|
+
...original,
|
|
493
|
+
default_model: nullToUndefined(data.default_model),
|
|
494
|
+
time_mode: nullToUndefined(data.time_mode),
|
|
495
|
+
name_display: nullToUndefined(data.name_display),
|
|
496
|
+
ceremony,
|
|
497
|
+
opencode,
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Validate that a model spec (e.g. "Anthropic:sonnet") references a real provider.
|
|
504
|
+
* Case-insensitive match — auto-corrects casing to the actual provider name.
|
|
505
|
+
* Throws if no matching provider found (caller's catch triggers re-edit).
|
|
506
|
+
*/
|
|
507
|
+
export function validateModelProvider(
|
|
508
|
+
modelSpec: string | undefined,
|
|
509
|
+
accounts: ProviderAccount[]
|
|
510
|
+
): string | undefined {
|
|
511
|
+
if (!modelSpec) return undefined;
|
|
512
|
+
|
|
513
|
+
const colonIdx = modelSpec.indexOf(":");
|
|
514
|
+
const providerPart = colonIdx >= 0 ? modelSpec.substring(0, colonIdx) : modelSpec;
|
|
515
|
+
const modelPart = colonIdx >= 0 ? modelSpec.substring(colonIdx + 1) : undefined;
|
|
516
|
+
|
|
517
|
+
const match = accounts.find(a => a.name.toLowerCase() === providerPart.toLowerCase());
|
|
518
|
+
|
|
519
|
+
if (!match) {
|
|
520
|
+
const available = accounts.map(a => a.name).join(", ");
|
|
521
|
+
throw new Error(
|
|
522
|
+
available
|
|
523
|
+
? `No provider named "${providerPart}". Available: ${available}`
|
|
524
|
+
: `No provider named "${providerPart}". Create one with /provider new`
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return modelPart ? `${match.name}:${modelPart}` : match.name;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// =============================================================================
|
|
532
|
+
// QUOTE SERIALIZATION
|
|
533
|
+
// =============================================================================
|
|
534
|
+
|
|
535
|
+
interface EditableQuote extends Quote {
|
|
536
|
+
_delete?: boolean;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
interface EditableQuoteData {
|
|
540
|
+
quotes: EditableQuote[];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export function quotesToYAML(quotes: Quote[]): string {
|
|
544
|
+
const data: EditableQuoteData = {
|
|
545
|
+
quotes: quotes.map(q => ({
|
|
546
|
+
...q,
|
|
547
|
+
_delete: false,
|
|
548
|
+
})),
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return YAML.stringify(data, {
|
|
552
|
+
lineWidth: 0,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export interface QuotesYAMLResult {
|
|
557
|
+
quotes: Quote[];
|
|
558
|
+
deletedQuoteIds: string[];
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function quotesFromYAML(yamlContent: string): QuotesYAMLResult {
|
|
562
|
+
const data = YAML.parse(yamlContent) as EditableQuoteData;
|
|
563
|
+
|
|
564
|
+
const deletedQuoteIds: string[] = [];
|
|
565
|
+
const quotes: Quote[] = [];
|
|
566
|
+
|
|
567
|
+
for (const q of data.quotes ?? []) {
|
|
568
|
+
if (q._delete) {
|
|
569
|
+
deletedQuoteIds.push(q.id);
|
|
570
|
+
} else {
|
|
571
|
+
const { _delete, ...quote } = q;
|
|
572
|
+
quotes.push(quote);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return {
|
|
577
|
+
quotes,
|
|
578
|
+
deletedQuoteIds,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
// =============================================================================
|
|
584
|
+
// PROVIDER ACCOUNT SERIALIZATION
|
|
585
|
+
// =============================================================================
|
|
586
|
+
|
|
587
|
+
interface EditableProviderData {
|
|
588
|
+
name: string;
|
|
589
|
+
type: "llm" | "storage";
|
|
590
|
+
url: string;
|
|
591
|
+
api_key?: string;
|
|
592
|
+
default_model?: string;
|
|
593
|
+
token_limit?: number | null;
|
|
594
|
+
extra_headers?: Record<string, string>;
|
|
595
|
+
enabled?: boolean;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
function resolveEnvVar(value: string | undefined): string | undefined {
|
|
600
|
+
if (!value || !value.startsWith("$")) return value;
|
|
601
|
+
const varName = value.slice(1);
|
|
602
|
+
return process.env[varName] || value;
|
|
603
|
+
}
|
|
604
|
+
const PLACEHOLDER_PROVIDER: EditableProviderData = {
|
|
605
|
+
name: "My Provider",
|
|
606
|
+
type: "llm",
|
|
607
|
+
url: "https://api.example.com/v1",
|
|
608
|
+
api_key: "your-api-key-or-$ENVAR",
|
|
609
|
+
default_model: "model-name",
|
|
610
|
+
token_limit: null,
|
|
611
|
+
extra_headers: {},
|
|
612
|
+
enabled: true,
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Generate YAML template for a NEW provider account
|
|
617
|
+
*/
|
|
618
|
+
export function newProviderToYAML(): string {
|
|
619
|
+
return YAML.stringify(PLACEHOLDER_PROVIDER, {
|
|
620
|
+
lineWidth: 0,
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Parse YAML for a NEW provider account
|
|
626
|
+
*/
|
|
627
|
+
export function newProviderFromYAML(yamlContent: string): ProviderAccount {
|
|
628
|
+
const data = YAML.parse(yamlContent) as EditableProviderData;
|
|
629
|
+
|
|
630
|
+
if (!data.name || data.name === PLACEHOLDER_PROVIDER.name) {
|
|
631
|
+
throw new Error("Provider name is required");
|
|
632
|
+
}
|
|
633
|
+
if (!data.url || data.url === PLACEHOLDER_PROVIDER.url) {
|
|
634
|
+
throw new Error("Provider URL is required");
|
|
635
|
+
}
|
|
636
|
+
if (data.api_key === PLACEHOLDER_PROVIDER.api_key) {
|
|
637
|
+
data.api_key = undefined;
|
|
638
|
+
}
|
|
639
|
+
if (data.default_model === PLACEHOLDER_PROVIDER.default_model) {
|
|
640
|
+
data.default_model = undefined;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
id: crypto.randomUUID(),
|
|
645
|
+
name: data.name,
|
|
646
|
+
type: (data.type === "storage" ? "storage" : "llm") as ProviderType,
|
|
647
|
+
url: data.url,
|
|
648
|
+
api_key: resolveEnvVar(data.api_key),
|
|
649
|
+
default_model: data.default_model,
|
|
650
|
+
token_limit: data.token_limit ?? undefined,
|
|
651
|
+
extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined,
|
|
652
|
+
enabled: data.enabled ?? true,
|
|
653
|
+
created_at: new Date().toISOString(),
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Serialize existing provider account to YAML for editing
|
|
659
|
+
*/
|
|
660
|
+
export function providerToYAML(account: ProviderAccount): string {
|
|
661
|
+
const data: EditableProviderData = {
|
|
662
|
+
name: account.name,
|
|
663
|
+
type: account.type as "llm" | "storage",
|
|
664
|
+
url: account.url,
|
|
665
|
+
api_key: account.api_key,
|
|
666
|
+
default_model: account.default_model,
|
|
667
|
+
token_limit: account.token_limit ?? null,
|
|
668
|
+
extra_headers: account.extra_headers,
|
|
669
|
+
enabled: account.enabled ?? true,
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
return YAML.stringify(data, {
|
|
673
|
+
lineWidth: 0,
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Parse YAML for an existing provider account (preserves id and created_at)
|
|
679
|
+
*/
|
|
680
|
+
export function providerFromYAML(yamlContent: string, original: ProviderAccount): ProviderAccount {
|
|
681
|
+
const data = YAML.parse(yamlContent) as EditableProviderData;
|
|
682
|
+
|
|
683
|
+
if (!data.name) {
|
|
684
|
+
throw new Error("Provider name is required");
|
|
685
|
+
}
|
|
686
|
+
if (!data.url) {
|
|
687
|
+
throw new Error("Provider URL is required");
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
id: original.id,
|
|
692
|
+
name: data.name,
|
|
693
|
+
type: (data.type === "storage" ? "storage" : "llm") as ProviderType,
|
|
694
|
+
url: data.url,
|
|
695
|
+
api_key: resolveEnvVar(data.api_key),
|
|
696
|
+
default_model: data.default_model,
|
|
697
|
+
token_limit: data.token_limit ?? undefined,
|
|
698
|
+
extra_headers: data.extra_headers && Object.keys(data.extra_headers).length > 0 ? data.extra_headers : undefined,
|
|
699
|
+
enabled: data.enabled ?? true,
|
|
700
|
+
created_at: original.created_at,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// =============================================================================
|
|
705
|
+
// CONTEXT / MESSAGE SERIALIZATION
|
|
706
|
+
// =============================================================================
|
|
707
|
+
|
|
708
|
+
interface EditableMessage {
|
|
709
|
+
id: string;
|
|
710
|
+
role: "human" | "system";
|
|
711
|
+
timestamp: string;
|
|
712
|
+
context_status: ContextStatus;
|
|
713
|
+
_delete?: boolean;
|
|
714
|
+
content: string;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
export function contextToYAML(messages: Message[]): string {
|
|
718
|
+
const header = [
|
|
719
|
+
"# context_status: default | always | never",
|
|
720
|
+
"# _delete: true — permanently removes the message",
|
|
721
|
+
].join("\n");
|
|
722
|
+
|
|
723
|
+
const data: EditableMessage[] = messages.map((m) => ({
|
|
724
|
+
id: m.id,
|
|
725
|
+
role: m.role,
|
|
726
|
+
timestamp: m.timestamp,
|
|
727
|
+
context_status: m.context_status,
|
|
728
|
+
_delete: false,
|
|
729
|
+
content: m.content,
|
|
730
|
+
}));
|
|
731
|
+
|
|
732
|
+
return header + "\n" + YAML.stringify(data, { lineWidth: 0 });
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
export interface ContextYAMLResult {
|
|
736
|
+
messages: Array<{ id: string; context_status: ContextStatus }>;
|
|
737
|
+
deletedMessageIds: string[];
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
export function contextFromYAML(yamlContent: string): ContextYAMLResult {
|
|
741
|
+
const data = YAML.parse(yamlContent) as EditableMessage[];
|
|
742
|
+
|
|
743
|
+
const deletedMessageIds: string[] = [];
|
|
744
|
+
const messages: Array<{ id: string; context_status: ContextStatus }> = [];
|
|
745
|
+
|
|
746
|
+
for (const m of data ?? []) {
|
|
747
|
+
if (m._delete) {
|
|
748
|
+
deletedMessageIds.push(m.id);
|
|
749
|
+
} else {
|
|
750
|
+
messages.push({ id: m.id, context_status: m.context_status });
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return { messages, deletedMessageIds };
|
|
755
|
+
}
|