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.
@@ -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 lines = [`# Valid types: ${BUILT_IN_IDENTIFIER_TYPES.join(', ')}`];
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
- export function humanToYAML(human: HumanEntity, personaLookup?: Map<string, string>, allGroups: string[] = []): string {
88
- const data: EditableHumanData = {
89
- facts: human.facts.map(f => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(f); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
90
- topics: human.topics.map(t => { const { interested_personas: _ip, persona_groups, ...rest } = readOnlyToEnd(t); return { ...rest, persona_groups: buildGroupCheckboxMap(persona_groups ?? [], allGroups), _delete: false }; }),
91
- people: human.people.map(p => {
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 personComment = knownTypesComment(personaLookup);
103
-
104
- return YAML.stringify(data, {
105
- lineWidth: 0,
106
- })
107
- .replace(/^(\s+)(learned_on: .+)$/mg, '$1# [read-only] $2')
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 (fact.description && !originalFact?.validated_date) {
217
- fact.validated_date = new Date().toISOString();
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
- changedTopicIds.add(topic.id);
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
- changedPersonIds.add(person.id);
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 | number;
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 === 'default' || data.heartbeat_delay_ms === undefined
179
+ heartbeat_delay_ms: data.heartbeat_delay_ms == null
179
180
  ? undefined
180
- : Number(data.heartbeat_delay_ms) || undefined,
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 || 'default',
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 === 'default' || data.heartbeat_delay_ms === undefined
328
+ heartbeat_delay_ms: data.heartbeat_delay_ms == null
328
329
  ? undefined
329
- : Number(data.heartbeat_delay_ms) || undefined,
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
- token_limit: m.token_limit,
52
- max_output_tokens: m.max_output_tokens,
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
- if (m.token_limit !== undefined) {
145
- modelLines.push(` token_limit: ${m.token_limit}`);
146
- }
147
- if (m.max_output_tokens !== undefined) {
148
- modelLines.push(` max_output_tokens: ${m.max_output_tokens}`);
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
- token_limit: m.token_limit,
192
- max_output_tokens: m.max_output_tokens,
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?: number | null;
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?: number | null;
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?: number | null;
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?: number | null;
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?: number | null;
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: nullToUndefined(data.opencode.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: nullToUndefined(data.claudeCode.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: nullToUndefined(data.cursor.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: nullToUndefined(data.backup.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: nullToUndefined(data.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),