ei-tui 0.6.4 → 0.6.6
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 +14 -7
- package/src/core/handlers/index.ts +1 -0
- package/src/core/llm-client.ts +12 -1
- package/src/core/orchestrators/human-extraction.ts +70 -11
- package/src/core/orchestrators/index.ts +2 -0
- package/src/core/processor.ts +3 -0
- package/src/core/queue-processor.ts +8 -5
- package/src/core/state-manager.ts +18 -0
- package/src/core/types/entities.ts +3 -1
- 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 +66 -23
- package/tui/src/components/HelpOverlay.tsx +63 -33
- package/tui/src/context/ei.tsx +9 -2
- package/tui/src/context/overlay.tsx +2 -0
- package/tui/src/index.tsx +7 -0
- package/tui/src/util/e2e-flags.ts +13 -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 +162 -40
- package/tui/src/util/yaml-persona.ts +11 -10
- package/tui/src/util/yaml-provider.ts +34 -9
- package/tui/src/util/yaml-settings.ts +21 -15
|
@@ -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 {
|
|
@@ -133,6 +225,9 @@ export interface HumanYAMLResult {
|
|
|
133
225
|
changedFactIds: Set<string>;
|
|
134
226
|
changedTopicIds: Set<string>;
|
|
135
227
|
changedPersonIds: Set<string>;
|
|
228
|
+
skippedFactCount: number;
|
|
229
|
+
skippedTopicCount: number;
|
|
230
|
+
skippedPersonCount: number;
|
|
136
231
|
}
|
|
137
232
|
|
|
138
233
|
function identifiersEqual(a: PersonIdentifier[] | undefined, b: PersonIdentifier[] | undefined): boolean {
|
|
@@ -187,12 +282,12 @@ function personChanged(parsed: Person, original: Person): boolean {
|
|
|
187
282
|
return !identifiersEqual(parsed.identifiers, original.identifiers);
|
|
188
283
|
}
|
|
189
284
|
|
|
190
|
-
export function humanFromYAML(yamlContent: string, original?: HumanEntity): HumanYAMLResult {
|
|
285
|
+
export function humanFromYAML(yamlContent: string, original?: HumanEntity, current?: HumanEntity): HumanYAMLResult {
|
|
191
286
|
const stripped = yamlContent
|
|
192
287
|
.split('\n')
|
|
193
288
|
.filter(line => !/^\s*#\s*\[read-only\]/.test(line))
|
|
194
289
|
.join('\n');
|
|
195
|
-
const data = YAML.parse(stripped) as EditableHumanData;
|
|
290
|
+
const data = (YAML.parse(stripped) ?? {}) as EditableHumanData;
|
|
196
291
|
|
|
197
292
|
const deletedFactIds: string[] = [];
|
|
198
293
|
const deletedTopicIds: string[] = [];
|
|
@@ -200,6 +295,15 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
200
295
|
const changedFactIds = new Set<string>();
|
|
201
296
|
const changedTopicIds = new Set<string>();
|
|
202
297
|
const changedPersonIds = new Set<string>();
|
|
298
|
+
let skippedFactCount = 0;
|
|
299
|
+
let skippedTopicCount = 0;
|
|
300
|
+
let skippedPersonCount = 0;
|
|
301
|
+
|
|
302
|
+
const staleInState = (id: string | undefined, originalItem: { last_updated: string } | undefined, currentItems: { id: string; last_updated: string }[] | undefined): boolean => {
|
|
303
|
+
if (!id || !originalItem || !current || !currentItems) return false;
|
|
304
|
+
const currentItem = currentItems.find(i => i.id === id);
|
|
305
|
+
return !!currentItem && currentItem.last_updated !== originalItem.last_updated;
|
|
306
|
+
};
|
|
203
307
|
|
|
204
308
|
const facts: Fact[] = [];
|
|
205
309
|
for (const f of data.facts ?? []) {
|
|
@@ -207,16 +311,21 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
207
311
|
deletedFactIds.push(f.id);
|
|
208
312
|
} else {
|
|
209
313
|
const { _delete, persona_groups: groupMap, ...parsed } = f;
|
|
314
|
+
if (!parsed.id) parsed.id = crypto.randomUUID();
|
|
210
315
|
const originalFact = original?.facts.find(of => of.id === parsed.id);
|
|
211
316
|
const fact: Fact = originalFact
|
|
212
317
|
? { ...originalFact, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
213
|
-
: { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
318
|
+
: { ...parsed, last_updated: new Date().toISOString(), persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
214
319
|
facts.push(fact);
|
|
215
320
|
if (!originalFact || factChanged(fact, originalFact)) {
|
|
216
|
-
if (
|
|
217
|
-
|
|
321
|
+
if (staleInState(parsed.id, originalFact, current?.facts)) {
|
|
322
|
+
skippedFactCount++;
|
|
323
|
+
} else {
|
|
324
|
+
if (fact.description && !originalFact?.validated_date) {
|
|
325
|
+
fact.validated_date = new Date().toISOString();
|
|
326
|
+
}
|
|
327
|
+
changedFactIds.add(fact.id);
|
|
218
328
|
}
|
|
219
|
-
changedFactIds.add(fact.id);
|
|
220
329
|
}
|
|
221
330
|
}
|
|
222
331
|
}
|
|
@@ -227,13 +336,18 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
227
336
|
deletedTopicIds.push(t.id);
|
|
228
337
|
} else {
|
|
229
338
|
const { _delete, persona_groups: groupMap, ...parsed } = t;
|
|
339
|
+
if (!parsed.id) parsed.id = crypto.randomUUID();
|
|
230
340
|
const originalTopic = original?.topics.find(ot => ot.id === parsed.id);
|
|
231
341
|
const topic: Topic = originalTopic
|
|
232
342
|
? { ...originalTopic, ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
233
|
-
: { ...parsed, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
343
|
+
: { ...parsed, last_updated: new Date().toISOString(), persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
234
344
|
topics.push(topic);
|
|
235
345
|
if (!originalTopic || topicChanged(topic, originalTopic)) {
|
|
236
|
-
|
|
346
|
+
if (staleInState(parsed.id, originalTopic, current?.topics)) {
|
|
347
|
+
skippedTopicCount++;
|
|
348
|
+
} else {
|
|
349
|
+
changedTopicIds.add(topic.id);
|
|
350
|
+
}
|
|
237
351
|
}
|
|
238
352
|
}
|
|
239
353
|
}
|
|
@@ -244,6 +358,7 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
244
358
|
deletedPersonIds.push(p.id);
|
|
245
359
|
} else {
|
|
246
360
|
const { _delete, identifiers: yamlIdentifiers, persona_groups: groupMap, ...parsed } = p;
|
|
361
|
+
if (!parsed.id) parsed.id = crypto.randomUUID();
|
|
247
362
|
const identifiers: PersonIdentifier[] = (yamlIdentifiers ?? []).map(({ type, value, primary }) => ({
|
|
248
363
|
type,
|
|
249
364
|
value,
|
|
@@ -252,10 +367,14 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
252
367
|
const originalPerson = original?.people.find(op => op.id === parsed.id);
|
|
253
368
|
const person: Person = originalPerson
|
|
254
369
|
? { ...originalPerson, ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) }
|
|
255
|
-
: { ...parsed, identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
370
|
+
: { ...parsed, last_updated: new Date().toISOString(), identifiers, persona_groups: parseGroupCheckboxMap(groupMap) };
|
|
256
371
|
people.push(person);
|
|
257
372
|
if (!originalPerson || personChanged(person, originalPerson)) {
|
|
258
|
-
|
|
373
|
+
if (staleInState(parsed.id, originalPerson, current?.people)) {
|
|
374
|
+
skippedPersonCount++;
|
|
375
|
+
} else {
|
|
376
|
+
changedPersonIds.add(person.id);
|
|
377
|
+
}
|
|
259
378
|
}
|
|
260
379
|
}
|
|
261
380
|
}
|
|
@@ -270,5 +389,8 @@ export function humanFromYAML(yamlContent: string, original?: HumanEntity): Huma
|
|
|
270
389
|
changedFactIds,
|
|
271
390
|
changedTopicIds,
|
|
272
391
|
changedPersonIds,
|
|
392
|
+
skippedFactCount,
|
|
393
|
+
skippedTopicCount,
|
|
394
|
+
skippedPersonCount,
|
|
273
395
|
};
|
|
274
396
|
}
|
|
@@ -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,
|
|
@@ -5,10 +5,15 @@ import type {
|
|
|
5
5
|
} from "../../../src/core/types.js";
|
|
6
6
|
import { modelGuidToDisplay } from "./yaml-shared.js";
|
|
7
7
|
|
|
8
|
+
const tokenFormatter = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 1 });
|
|
9
|
+
const formatTokens = (n: number) => tokenFormatter.format(n);
|
|
10
|
+
|
|
8
11
|
interface EditableModelData {
|
|
9
12
|
name: string;
|
|
13
|
+
model_id?: string;
|
|
10
14
|
token_limit?: number;
|
|
11
15
|
max_output_tokens?: number;
|
|
16
|
+
thinking_budget?: number;
|
|
12
17
|
_delete?: boolean;
|
|
13
18
|
}
|
|
14
19
|
|
|
@@ -45,11 +50,14 @@ function parseModels(editableModels: EditableModelData[]): import('../../../src/
|
|
|
45
50
|
const result: import('../../../src/core/types.js').ModelConfig[] = [];
|
|
46
51
|
for (const m of editableModels) {
|
|
47
52
|
if (m._delete) continue;
|
|
53
|
+
const modelId = m.model_id ?? undefined;
|
|
48
54
|
result.push({
|
|
49
55
|
id: crypto.randomUUID(),
|
|
50
56
|
name: m.name,
|
|
51
|
-
|
|
52
|
-
|
|
57
|
+
model_id: (modelId === null || modelId === m.name) ? undefined : modelId,
|
|
58
|
+
token_limit: m.token_limit ?? undefined,
|
|
59
|
+
max_output_tokens: m.max_output_tokens ?? undefined,
|
|
60
|
+
thinking_budget: m.thinking_budget ?? undefined,
|
|
53
61
|
});
|
|
54
62
|
}
|
|
55
63
|
return result;
|
|
@@ -70,6 +78,10 @@ export function newProviderToYAML(name?: string): string {
|
|
|
70
78
|
const modelsYAML = [
|
|
71
79
|
"models:",
|
|
72
80
|
" - name: (default)",
|
|
81
|
+
" model_id: (default)",
|
|
82
|
+
" token_limit: null",
|
|
83
|
+
" max_output_tokens: null",
|
|
84
|
+
" thinking_budget: null",
|
|
73
85
|
" # _delete: true",
|
|
74
86
|
"# _delete: true # Delete this entire provider",
|
|
75
87
|
].join("\n");
|
|
@@ -141,16 +153,26 @@ export function providerToYAML(account: ProviderAccount): string {
|
|
|
141
153
|
if (modelList.length > 0) {
|
|
142
154
|
for (const m of modelList) {
|
|
143
155
|
modelLines.push(` - name: ${m.name}`);
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
modelLines.push(` model_id: ${m.model_id ?? m.name}`);
|
|
157
|
+
modelLines.push(` token_limit: ${m.token_limit ?? null}`);
|
|
158
|
+
modelLines.push(` max_output_tokens: ${m.max_output_tokens ?? null}`);
|
|
159
|
+
modelLines.push(` thinking_budget: ${m.thinking_budget ?? null}`);
|
|
160
|
+
if (m.total_calls !== undefined || m.total_tokens_in !== undefined) {
|
|
161
|
+
const tokensIn = m.total_tokens_in ?? 0;
|
|
162
|
+
const tokensOut = m.total_tokens_out ?? 0;
|
|
163
|
+
modelLines.push(` # stats: ${formatTokens(m.total_calls ?? 0)} calls · ${formatTokens(tokensIn)} in / ${formatTokens(tokensOut)} out`);
|
|
164
|
+
if (m.last_used) {
|
|
165
|
+
modelLines.push(` # used: ${m.last_used}`);
|
|
166
|
+
}
|
|
149
167
|
}
|
|
150
168
|
modelLines.push(` _delete: false`);
|
|
151
169
|
}
|
|
152
170
|
} else {
|
|
153
171
|
modelLines.push(" - name: (default)");
|
|
172
|
+
modelLines.push(` model_id: (default)`);
|
|
173
|
+
modelLines.push(` token_limit: null`);
|
|
174
|
+
modelLines.push(` max_output_tokens: null`);
|
|
175
|
+
modelLines.push(` thinking_budget: null`);
|
|
154
176
|
modelLines.push(" _delete: false");
|
|
155
177
|
}
|
|
156
178
|
modelLines.push("_delete: false # Set to true to delete this entire provider");
|
|
@@ -185,11 +207,14 @@ export function providerFromYAML(yamlContent: string, original: ProviderAccount)
|
|
|
185
207
|
for (const m of data.models ?? []) {
|
|
186
208
|
if (m._delete) continue;
|
|
187
209
|
const existing = existingModels.find(em => em.name === m.name);
|
|
210
|
+
const modelId = m.model_id ?? undefined;
|
|
188
211
|
parsedModels.push({
|
|
189
212
|
id: existing?.id ?? crypto.randomUUID(),
|
|
190
213
|
name: m.name,
|
|
191
|
-
|
|
192
|
-
|
|
214
|
+
model_id: (modelId === null || modelId === m.name) ? undefined : modelId,
|
|
215
|
+
token_limit: m.token_limit ?? undefined,
|
|
216
|
+
max_output_tokens: m.max_output_tokens ?? undefined,
|
|
217
|
+
thinking_budget: m.thinking_budget ?? undefined,
|
|
193
218
|
total_calls: existing?.total_calls,
|
|
194
219
|
total_tokens_in: existing?.total_tokens_in,
|
|
195
220
|
total_tokens_out: existing?.total_tokens_out,
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
import type { ClaudeCodeSettings } from "../../../src/integrations/claude-code/types.js";
|
|
9
9
|
import type { CursorSettings } from "../../../src/integrations/cursor/types.js";
|
|
10
10
|
import { modelGuidToDisplay, displayToModelGuid } from "./yaml-shared.js";
|
|
11
|
+
import { parseDuration, formatDuration } from "./duration.js";
|
|
11
12
|
|
|
12
13
|
interface EditableSettingsData {
|
|
13
14
|
default_model?: string | null;
|
|
@@ -15,7 +16,7 @@ interface EditableSettingsData {
|
|
|
15
16
|
rewrite_model?: string | null;
|
|
16
17
|
time_mode?: "24h" | "12h" | "local" | "utc" | null;
|
|
17
18
|
name_display?: string | null;
|
|
18
|
-
default_heartbeat_ms?:
|
|
19
|
+
default_heartbeat_ms?: string | null;
|
|
19
20
|
default_context_window_hours?: number | null;
|
|
20
21
|
message_min_count?: number | null;
|
|
21
22
|
message_max_age_days?: number | null;
|
|
@@ -28,28 +29,28 @@ interface EditableSettingsData {
|
|
|
28
29
|
};
|
|
29
30
|
opencode?: {
|
|
30
31
|
integration?: boolean | null;
|
|
31
|
-
polling_interval_ms?:
|
|
32
|
+
polling_interval_ms?: string | null;
|
|
32
33
|
last_sync?: string | null;
|
|
33
34
|
extraction_point?: string | null;
|
|
34
35
|
extraction_model?: string | null;
|
|
35
36
|
};
|
|
36
37
|
claudeCode?: {
|
|
37
38
|
integration?: boolean | null;
|
|
38
|
-
polling_interval_ms?:
|
|
39
|
+
polling_interval_ms?: string | null;
|
|
39
40
|
last_sync?: string | null;
|
|
40
41
|
extraction_point?: string | null;
|
|
41
42
|
extraction_model?: string | null;
|
|
42
43
|
};
|
|
43
44
|
cursor?: {
|
|
44
45
|
integration?: boolean | null;
|
|
45
|
-
polling_interval_ms?:
|
|
46
|
+
polling_interval_ms?: string | null;
|
|
46
47
|
last_sync?: string | null;
|
|
47
48
|
extraction_point?: string | null;
|
|
48
49
|
};
|
|
49
50
|
backup?: {
|
|
50
51
|
enabled?: boolean | null;
|
|
51
52
|
max_backups?: number | null;
|
|
52
|
-
interval_ms?:
|
|
53
|
+
interval_ms?: string | null;
|
|
53
54
|
};
|
|
54
55
|
}
|
|
55
56
|
|
|
@@ -65,7 +66,7 @@ export function settingsToYAML(settings: HumanSettings | undefined, accounts: Pr
|
|
|
65
66
|
rewrite_model: guidToDisplay(settings?.rewrite_model),
|
|
66
67
|
time_mode: settings?.time_mode ?? null,
|
|
67
68
|
name_display: settings?.name_display ?? null,
|
|
68
|
-
default_heartbeat_ms: settings?.default_heartbeat_ms ?? 1800000,
|
|
69
|
+
default_heartbeat_ms: formatDuration(settings?.default_heartbeat_ms ?? 1800000),
|
|
69
70
|
default_context_window_hours: settings?.default_context_window_hours ?? 8,
|
|
70
71
|
message_min_count: settings?.message_min_count ?? 200,
|
|
71
72
|
message_max_age_days: settings?.message_max_age_days ?? 14,
|
|
@@ -78,28 +79,28 @@ export function settingsToYAML(settings: HumanSettings | undefined, accounts: Pr
|
|
|
78
79
|
},
|
|
79
80
|
opencode: {
|
|
80
81
|
integration: settings?.opencode?.integration ?? false,
|
|
81
|
-
polling_interval_ms: settings?.opencode?.polling_interval_ms ?? 60000,
|
|
82
|
+
polling_interval_ms: formatDuration(settings?.opencode?.polling_interval_ms ?? 60000),
|
|
82
83
|
extraction_model: guidToDisplay(settings?.opencode?.extraction_model) ?? 'default',
|
|
83
84
|
last_sync: settings?.opencode?.last_sync ?? null,
|
|
84
85
|
extraction_point: settings?.opencode?.extraction_point ?? null,
|
|
85
86
|
},
|
|
86
87
|
claudeCode: {
|
|
87
88
|
integration: settings?.claudeCode?.integration ?? false,
|
|
88
|
-
polling_interval_ms: settings?.claudeCode?.polling_interval_ms ?? 60000,
|
|
89
|
+
polling_interval_ms: formatDuration(settings?.claudeCode?.polling_interval_ms ?? 60000),
|
|
89
90
|
extraction_model: guidToDisplay(settings?.claudeCode?.extraction_model) ?? 'default',
|
|
90
91
|
last_sync: settings?.claudeCode?.last_sync ?? null,
|
|
91
92
|
extraction_point: settings?.claudeCode?.extraction_point ?? null,
|
|
92
93
|
},
|
|
93
94
|
cursor: {
|
|
94
95
|
integration: settings?.cursor?.integration ?? false,
|
|
95
|
-
polling_interval_ms: settings?.cursor?.polling_interval_ms ?? 60000,
|
|
96
|
+
polling_interval_ms: formatDuration(settings?.cursor?.polling_interval_ms ?? 60000),
|
|
96
97
|
last_sync: settings?.cursor?.last_sync ?? null,
|
|
97
98
|
extraction_point: settings?.cursor?.extraction_point ?? null,
|
|
98
99
|
},
|
|
99
100
|
backup: {
|
|
100
101
|
enabled: settings?.backup?.enabled ?? false,
|
|
101
102
|
max_backups: settings?.backup?.max_backups ?? 24,
|
|
102
|
-
interval_ms: settings?.backup?.interval_ms ?? 3600000,
|
|
103
|
+
interval_ms: formatDuration(settings?.backup?.interval_ms ?? 3600000),
|
|
103
104
|
},
|
|
104
105
|
};
|
|
105
106
|
|
|
@@ -117,6 +118,11 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
117
118
|
const nullToUndefined = <T>(value: T | null | undefined): T | undefined =>
|
|
118
119
|
value === null ? undefined : value;
|
|
119
120
|
|
|
121
|
+
const parseMsDuration = (value: string | null | undefined, fallback: number): number | undefined => {
|
|
122
|
+
if (value == null) return undefined;
|
|
123
|
+
return parseDuration(value) ?? fallback;
|
|
124
|
+
};
|
|
125
|
+
|
|
120
126
|
const displayToGuid = (display: string | null | undefined): string | undefined => {
|
|
121
127
|
if (!display || display === 'default') return undefined;
|
|
122
128
|
return displayToModelGuid(display, accounts) ?? display;
|
|
@@ -138,7 +144,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
138
144
|
if (data.opencode) {
|
|
139
145
|
opencode = {
|
|
140
146
|
integration: nullToUndefined(data.opencode.integration),
|
|
141
|
-
polling_interval_ms:
|
|
147
|
+
polling_interval_ms: parseMsDuration(data.opencode.polling_interval_ms, 60000),
|
|
142
148
|
last_sync: original?.opencode?.last_sync,
|
|
143
149
|
extraction_point: original?.opencode?.extraction_point,
|
|
144
150
|
processed_sessions: original?.opencode?.processed_sessions,
|
|
@@ -150,7 +156,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
150
156
|
if (data.claudeCode) {
|
|
151
157
|
claudeCode = {
|
|
152
158
|
integration: nullToUndefined(data.claudeCode.integration),
|
|
153
|
-
polling_interval_ms:
|
|
159
|
+
polling_interval_ms: parseMsDuration(data.claudeCode.polling_interval_ms, 60000),
|
|
154
160
|
last_sync: original?.claudeCode?.last_sync,
|
|
155
161
|
extraction_point: original?.claudeCode?.extraction_point,
|
|
156
162
|
processed_sessions: original?.claudeCode?.processed_sessions,
|
|
@@ -162,7 +168,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
162
168
|
if (data.cursor) {
|
|
163
169
|
cursor = {
|
|
164
170
|
integration: nullToUndefined(data.cursor.integration),
|
|
165
|
-
polling_interval_ms:
|
|
171
|
+
polling_interval_ms: parseMsDuration(data.cursor.polling_interval_ms, 60000),
|
|
166
172
|
last_sync: original?.cursor?.last_sync,
|
|
167
173
|
extraction_point: original?.cursor?.extraction_point,
|
|
168
174
|
processed_sessions: original?.cursor?.processed_sessions,
|
|
@@ -174,7 +180,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
174
180
|
backup = {
|
|
175
181
|
enabled: nullToUndefined(data.backup.enabled),
|
|
176
182
|
max_backups: nullToUndefined(data.backup.max_backups),
|
|
177
|
-
interval_ms:
|
|
183
|
+
interval_ms: parseMsDuration(data.backup.interval_ms, 3600000),
|
|
178
184
|
last_backup: original?.backup?.last_backup,
|
|
179
185
|
};
|
|
180
186
|
}
|
|
@@ -186,7 +192,7 @@ export function settingsFromYAML(yamlContent: string, original: HumanSettings |
|
|
|
186
192
|
rewrite_model: displayToGuid(data.rewrite_model),
|
|
187
193
|
time_mode: nullToUndefined(data.time_mode),
|
|
188
194
|
name_display: nullToUndefined(data.name_display),
|
|
189
|
-
default_heartbeat_ms:
|
|
195
|
+
default_heartbeat_ms: parseMsDuration(data.default_heartbeat_ms, 1800000),
|
|
190
196
|
default_context_window_hours: nullToUndefined(data.default_context_window_hours),
|
|
191
197
|
message_min_count: nullToUndefined(data.message_min_count),
|
|
192
198
|
message_max_age_days: nullToUndefined(data.message_max_age_days),
|