@monotykamary/pi-better-openai 0.1.23
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 +191 -0
- package/THIRD_PARTY_NOTICES.md +28 -0
- package/index.ts +1328 -0
- package/package.json +94 -0
- package/src/codex-auth.ts +136 -0
- package/src/config.ts +725 -0
- package/src/fast-controller.ts +112 -0
- package/src/footer-layout.ts +97 -0
- package/src/format.ts +104 -0
- package/src/identity.ts +7 -0
- package/src/image.ts +672 -0
- package/src/live/attestation.ts +86 -0
- package/src/live/controller.ts +523 -0
- package/src/live/index.ts +197 -0
- package/src/live/native.ts +109 -0
- package/src/live/protocol.ts +234 -0
- package/src/live/transport.ts +445 -0
- package/src/live/visualizer.ts +269 -0
- package/src/live/voices.ts +21 -0
- package/src/paths.ts +17 -0
- package/src/pet-footer-controller.ts +642 -0
- package/src/pets.ts +983 -0
- package/src/usage-controller.ts +304 -0
- package/src/usage.ts +264 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { CONFIG_BASENAME, logPrefix } from "./identity.ts";
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_LIVE_VOICE,
|
|
7
|
+
isLiveVoice,
|
|
8
|
+
LIVE_VOICE_VALUES,
|
|
9
|
+
type LiveVoice,
|
|
10
|
+
} from "./live/voices.ts";
|
|
11
|
+
import { piAgentDir } from "./paths.ts";
|
|
12
|
+
|
|
13
|
+
export const FOOTER_MODES = ["replace", "status", "off"] as const;
|
|
14
|
+
export const IMAGE_SAVE_MODES = ["none", "project", "global", "custom"] as const;
|
|
15
|
+
export const IMAGE_OUTPUT_FORMATS = ["png", "jpeg", "webp"] as const;
|
|
16
|
+
export const PET_PLACEMENTS = [
|
|
17
|
+
"stacked",
|
|
18
|
+
"inline-left",
|
|
19
|
+
"inline-right",
|
|
20
|
+
"badge",
|
|
21
|
+
"habitat",
|
|
22
|
+
] as const;
|
|
23
|
+
export const PET_STATES = [
|
|
24
|
+
"idle",
|
|
25
|
+
"running-right",
|
|
26
|
+
"running-left",
|
|
27
|
+
"waving",
|
|
28
|
+
"jumping",
|
|
29
|
+
"failed",
|
|
30
|
+
"waiting",
|
|
31
|
+
"running",
|
|
32
|
+
"review",
|
|
33
|
+
] as const;
|
|
34
|
+
|
|
35
|
+
export const DEFAULT_SUPPORTED_MODELS = [
|
|
36
|
+
"openai/gpt-5.4",
|
|
37
|
+
"openai/gpt-5.5",
|
|
38
|
+
"openai-codex/gpt-5.6-sol",
|
|
39
|
+
"openai-codex/gpt-5.6-terra",
|
|
40
|
+
"openai-codex/gpt-5.6-luna",
|
|
41
|
+
"openai-codex/gpt-5.4",
|
|
42
|
+
"openai-codex/gpt-5.5",
|
|
43
|
+
] as const;
|
|
44
|
+
|
|
45
|
+
export type FooterMode = (typeof FOOTER_MODES)[number];
|
|
46
|
+
export type ImageSaveMode = (typeof IMAGE_SAVE_MODES)[number];
|
|
47
|
+
export type ImageOutputFormat = (typeof IMAGE_OUTPUT_FORMATS)[number];
|
|
48
|
+
export type PetPlacement = (typeof PET_PLACEMENTS)[number];
|
|
49
|
+
export type PetState = (typeof PET_STATES)[number];
|
|
50
|
+
|
|
51
|
+
export type UsageConfig = {
|
|
52
|
+
enabled?: boolean;
|
|
53
|
+
refreshIntervalMs?: number;
|
|
54
|
+
showOnlyOnSubscriptionModels?: boolean;
|
|
55
|
+
showResetTimes?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export type FooterConfig = {
|
|
59
|
+
mode?: FooterMode;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type ImageConfig = {
|
|
63
|
+
enabled?: boolean;
|
|
64
|
+
defaultModel?: string;
|
|
65
|
+
defaultSave?: ImageSaveMode;
|
|
66
|
+
outputFormat?: ImageOutputFormat;
|
|
67
|
+
timeoutMs?: number;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
export type LiveConfig = {
|
|
71
|
+
enabled?: boolean;
|
|
72
|
+
voice?: LiveVoice;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export type PetConfig = {
|
|
76
|
+
enabled?: boolean;
|
|
77
|
+
slug?: string;
|
|
78
|
+
placement?: PetPlacement;
|
|
79
|
+
state?: PetState;
|
|
80
|
+
thinkingState?: PetState;
|
|
81
|
+
toolState?: PetState;
|
|
82
|
+
failedToolState?: PetState;
|
|
83
|
+
idleEmotes?: boolean;
|
|
84
|
+
idleEmoteIntervalMs?: number;
|
|
85
|
+
sizeCells?: number;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export interface ConfigFile {
|
|
89
|
+
persistState?: boolean;
|
|
90
|
+
active?: boolean;
|
|
91
|
+
desiredActive?: boolean;
|
|
92
|
+
supportedModels?: string[];
|
|
93
|
+
usage?: UsageConfig;
|
|
94
|
+
footer?: FooterConfig;
|
|
95
|
+
image?: ImageConfig;
|
|
96
|
+
live?: LiveConfig;
|
|
97
|
+
pets?: PetConfig;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface SupportedModel {
|
|
101
|
+
provider: string;
|
|
102
|
+
id: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ResolvedConfig {
|
|
106
|
+
configPath: string;
|
|
107
|
+
projectConfigPath: string;
|
|
108
|
+
globalConfigPath: string;
|
|
109
|
+
projectConfigExists: boolean;
|
|
110
|
+
globalConfigExists: boolean;
|
|
111
|
+
persistState: boolean;
|
|
112
|
+
active: boolean;
|
|
113
|
+
desiredActive: boolean;
|
|
114
|
+
supportedModels: SupportedModel[];
|
|
115
|
+
usage: Required<UsageConfig>;
|
|
116
|
+
footer: Required<FooterConfig>;
|
|
117
|
+
image: Required<ImageConfig>;
|
|
118
|
+
live: Required<LiveConfig>;
|
|
119
|
+
pets: Required<PetConfig>;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const DEFAULT_USAGE_CONFIG: Required<UsageConfig> = {
|
|
123
|
+
enabled: true,
|
|
124
|
+
refreshIntervalMs: 60_000,
|
|
125
|
+
showOnlyOnSubscriptionModels: true,
|
|
126
|
+
showResetTimes: true,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export const DEFAULT_FOOTER_CONFIG: Required<FooterConfig> = {
|
|
130
|
+
mode: "replace",
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export const DEFAULT_IMAGE_CONFIG: Required<ImageConfig> = {
|
|
134
|
+
enabled: true,
|
|
135
|
+
defaultModel: "gpt-5.5",
|
|
136
|
+
defaultSave: "project",
|
|
137
|
+
outputFormat: "png",
|
|
138
|
+
timeoutMs: 180_000,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export const DEFAULT_LIVE_CONFIG: Required<LiveConfig> = {
|
|
142
|
+
enabled: true,
|
|
143
|
+
voice: DEFAULT_LIVE_VOICE,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const DEFAULT_PET_CONFIG: Required<PetConfig> = {
|
|
147
|
+
enabled: false,
|
|
148
|
+
slug: "",
|
|
149
|
+
placement: "inline-right",
|
|
150
|
+
state: "idle",
|
|
151
|
+
thinkingState: "review",
|
|
152
|
+
toolState: "running",
|
|
153
|
+
failedToolState: "failed",
|
|
154
|
+
idleEmotes: true,
|
|
155
|
+
idleEmoteIntervalMs: 30_000,
|
|
156
|
+
sizeCells: 10,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const DEFAULT_CONFIG: ConfigFile = {
|
|
160
|
+
persistState: true,
|
|
161
|
+
active: false,
|
|
162
|
+
desiredActive: false,
|
|
163
|
+
supportedModels: [...DEFAULT_SUPPORTED_MODELS],
|
|
164
|
+
usage: DEFAULT_USAGE_CONFIG,
|
|
165
|
+
footer: DEFAULT_FOOTER_CONFIG,
|
|
166
|
+
image: DEFAULT_IMAGE_CONFIG,
|
|
167
|
+
live: DEFAULT_LIVE_CONFIG,
|
|
168
|
+
pets: DEFAULT_PET_CONFIG,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export type SettingsOptionSection = "root" | "usage" | "footer" | "image" | "live" | "pets";
|
|
172
|
+
|
|
173
|
+
export type SettingsValueContext = {
|
|
174
|
+
petEmptyValue?: string;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export type SettingsOptionDescriptor = {
|
|
178
|
+
id: string;
|
|
179
|
+
section: SettingsOptionSection;
|
|
180
|
+
key: string;
|
|
181
|
+
label: string;
|
|
182
|
+
description: string;
|
|
183
|
+
values?: readonly string[];
|
|
184
|
+
parse(rawValue: string, context?: SettingsValueContext): boolean | number | string;
|
|
185
|
+
currentValue(cfg: ResolvedConfig): string;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const booleanSetting = (rawValue: string): boolean => rawValue === "true";
|
|
189
|
+
const numberSetting = (rawValue: string): number => Number(rawValue);
|
|
190
|
+
const stringSetting = (rawValue: string): string => rawValue;
|
|
191
|
+
|
|
192
|
+
export const FAST_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
193
|
+
{
|
|
194
|
+
id: "persistState",
|
|
195
|
+
section: "root",
|
|
196
|
+
key: "persistState",
|
|
197
|
+
label: "Persist fast state",
|
|
198
|
+
currentValue: (cfg) => String(cfg.persistState),
|
|
199
|
+
values: ["true", "false"],
|
|
200
|
+
description: "Remember fast-mode state across sessions.",
|
|
201
|
+
parse: booleanSetting,
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
export const FOOTER_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
206
|
+
{
|
|
207
|
+
id: "footer.mode",
|
|
208
|
+
section: "footer",
|
|
209
|
+
key: "mode",
|
|
210
|
+
label: "Footer mode",
|
|
211
|
+
currentValue: (cfg) => cfg.footer.mode,
|
|
212
|
+
values: FOOTER_MODES,
|
|
213
|
+
description:
|
|
214
|
+
"replace = custom footer, status = pi footer plus status line, off = no Better OpenAI footer/status unless Footer pet is enabled.",
|
|
215
|
+
parse: stringSetting,
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
export const USAGE_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
220
|
+
{
|
|
221
|
+
id: "usage.enabled",
|
|
222
|
+
section: "usage",
|
|
223
|
+
key: "enabled",
|
|
224
|
+
label: "Usage display",
|
|
225
|
+
currentValue: (cfg) => String(cfg.usage.enabled),
|
|
226
|
+
values: ["true", "false"],
|
|
227
|
+
description: "Fetch and display OpenAI subscription usage windows.",
|
|
228
|
+
parse: booleanSetting,
|
|
229
|
+
},
|
|
230
|
+
{
|
|
231
|
+
id: "usage.refreshIntervalMs",
|
|
232
|
+
section: "usage",
|
|
233
|
+
key: "refreshIntervalMs",
|
|
234
|
+
label: "Usage refresh",
|
|
235
|
+
currentValue: (cfg) => String(cfg.usage.refreshIntervalMs),
|
|
236
|
+
values: ["15000", "30000", "60000", "120000", "300000", "600000"],
|
|
237
|
+
description: "Usage refresh interval in milliseconds.",
|
|
238
|
+
parse: numberSetting,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: "usage.showOnlyOnSubscriptionModels",
|
|
242
|
+
section: "usage",
|
|
243
|
+
key: "showOnlyOnSubscriptionModels",
|
|
244
|
+
label: "Usage only on OAuth",
|
|
245
|
+
currentValue: (cfg) => String(cfg.usage.showOnlyOnSubscriptionModels),
|
|
246
|
+
values: ["true", "false"],
|
|
247
|
+
description: "Only show usage when the current OpenAI model uses subscription/OAuth auth.",
|
|
248
|
+
parse: booleanSetting,
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
id: "usage.showResetTimes",
|
|
252
|
+
section: "usage",
|
|
253
|
+
key: "showResetTimes",
|
|
254
|
+
label: "Usage reset times",
|
|
255
|
+
currentValue: (cfg) => String(cfg.usage.showResetTimes),
|
|
256
|
+
values: ["true", "false"],
|
|
257
|
+
description: "Include compact reset countdowns and local reset times.",
|
|
258
|
+
parse: booleanSetting,
|
|
259
|
+
},
|
|
260
|
+
];
|
|
261
|
+
|
|
262
|
+
export const IMAGE_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
263
|
+
{
|
|
264
|
+
id: "image.enabled",
|
|
265
|
+
section: "image",
|
|
266
|
+
key: "enabled",
|
|
267
|
+
label: "Image tool",
|
|
268
|
+
currentValue: (cfg) => String(cfg.image.enabled),
|
|
269
|
+
values: ["true", "false"],
|
|
270
|
+
description: "Allow the openai_image tool to make image requests.",
|
|
271
|
+
parse: booleanSetting,
|
|
272
|
+
},
|
|
273
|
+
{
|
|
274
|
+
id: "image.defaultModel",
|
|
275
|
+
section: "image",
|
|
276
|
+
key: "defaultModel",
|
|
277
|
+
label: "Image model",
|
|
278
|
+
currentValue: (cfg) => cfg.image.defaultModel,
|
|
279
|
+
values: ["gpt-5.5", "gpt-5.4", "gpt-5.2", "gpt-5"],
|
|
280
|
+
description: "Mainline model used for image generation when current model is not openai-codex.",
|
|
281
|
+
parse: stringSetting,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: "image.defaultSave",
|
|
285
|
+
section: "image",
|
|
286
|
+
key: "defaultSave",
|
|
287
|
+
label: "Image save",
|
|
288
|
+
currentValue: (cfg) => cfg.image.defaultSave,
|
|
289
|
+
values: IMAGE_SAVE_MODES,
|
|
290
|
+
description: "Where generated images are saved by default.",
|
|
291
|
+
parse: stringSetting,
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
id: "image.outputFormat",
|
|
295
|
+
section: "image",
|
|
296
|
+
key: "outputFormat",
|
|
297
|
+
label: "Image format",
|
|
298
|
+
currentValue: (cfg) => cfg.image.outputFormat,
|
|
299
|
+
values: IMAGE_OUTPUT_FORMATS,
|
|
300
|
+
description: "Generated image file format.",
|
|
301
|
+
parse: stringSetting,
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: "image.timeoutMs",
|
|
305
|
+
section: "image",
|
|
306
|
+
key: "timeoutMs",
|
|
307
|
+
label: "Image timeout",
|
|
308
|
+
currentValue: (cfg) => String(cfg.image.timeoutMs),
|
|
309
|
+
values: ["30000", "60000", "120000", "180000", "300000"],
|
|
310
|
+
description: "Image request timeout in milliseconds.",
|
|
311
|
+
parse: numberSetting,
|
|
312
|
+
},
|
|
313
|
+
];
|
|
314
|
+
|
|
315
|
+
export const LIVE_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
316
|
+
{
|
|
317
|
+
id: "live.enabled",
|
|
318
|
+
section: "live",
|
|
319
|
+
key: "enabled",
|
|
320
|
+
label: "Live voice",
|
|
321
|
+
currentValue: (cfg) => String(cfg.live.enabled),
|
|
322
|
+
values: ["true", "false"],
|
|
323
|
+
description: "Allow /live to start a Codex-backed realtime voice session.",
|
|
324
|
+
parse: booleanSetting,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
id: "live.voice",
|
|
328
|
+
section: "live",
|
|
329
|
+
key: "voice",
|
|
330
|
+
label: "Voice",
|
|
331
|
+
currentValue: (cfg) => cfg.live.voice,
|
|
332
|
+
values: LIVE_VOICE_VALUES,
|
|
333
|
+
description: "Voice used for realtime spoken responses.",
|
|
334
|
+
parse: stringSetting,
|
|
335
|
+
},
|
|
336
|
+
];
|
|
337
|
+
|
|
338
|
+
export const PET_SETTING_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
339
|
+
{
|
|
340
|
+
id: "pets.enabled",
|
|
341
|
+
section: "pets",
|
|
342
|
+
key: "enabled",
|
|
343
|
+
label: "Enabled",
|
|
344
|
+
currentValue: (cfg) => String(cfg.pets.enabled),
|
|
345
|
+
values: ["true", "false"],
|
|
346
|
+
description:
|
|
347
|
+
"Render a custom Codex pet from ${CODEX_HOME:-~/.codex}/pets in the Better OpenAI footer.",
|
|
348
|
+
parse: booleanSetting,
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
id: "pets.slug",
|
|
352
|
+
section: "pets",
|
|
353
|
+
key: "slug",
|
|
354
|
+
label: "Pet",
|
|
355
|
+
currentValue: (cfg) => cfg.pets.slug,
|
|
356
|
+
description: "Selected custom pet slug.",
|
|
357
|
+
parse: (rawValue, context) =>
|
|
358
|
+
rawValue === (context?.petEmptyValue ?? "not selected") ? "" : rawValue,
|
|
359
|
+
},
|
|
360
|
+
{
|
|
361
|
+
id: "pets.placement",
|
|
362
|
+
section: "pets",
|
|
363
|
+
key: "placement",
|
|
364
|
+
label: "Placement",
|
|
365
|
+
currentValue: (cfg) => cfg.pets.placement,
|
|
366
|
+
values: PET_PLACEMENTS,
|
|
367
|
+
description: "Footer layout: stacked, inline-left, inline-right, badge, or habitat divider.",
|
|
368
|
+
parse: stringSetting,
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
id: "pets.state",
|
|
372
|
+
section: "pets",
|
|
373
|
+
key: "state",
|
|
374
|
+
label: "Idle state",
|
|
375
|
+
currentValue: (cfg) => cfg.pets.state,
|
|
376
|
+
values: PET_STATES,
|
|
377
|
+
description: "Animation row to show when pi is idle.",
|
|
378
|
+
parse: stringSetting,
|
|
379
|
+
},
|
|
380
|
+
{
|
|
381
|
+
id: "pets.thinkingState",
|
|
382
|
+
section: "pets",
|
|
383
|
+
key: "thinkingState",
|
|
384
|
+
label: "Thinking state",
|
|
385
|
+
currentValue: (cfg) => cfg.pets.thinkingState,
|
|
386
|
+
values: PET_STATES,
|
|
387
|
+
description: "Animation row to show while the model is thinking or streaming.",
|
|
388
|
+
parse: stringSetting,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
id: "pets.toolState",
|
|
392
|
+
section: "pets",
|
|
393
|
+
key: "toolState",
|
|
394
|
+
label: "Tool state",
|
|
395
|
+
currentValue: (cfg) => cfg.pets.toolState,
|
|
396
|
+
values: PET_STATES,
|
|
397
|
+
description: "Animation row to show during tool execution.",
|
|
398
|
+
parse: stringSetting,
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: "pets.failedToolState",
|
|
402
|
+
section: "pets",
|
|
403
|
+
key: "failedToolState",
|
|
404
|
+
label: "Failed tool state",
|
|
405
|
+
currentValue: (cfg) => cfg.pets.failedToolState,
|
|
406
|
+
values: PET_STATES,
|
|
407
|
+
description: "Animation row to flash after any tool call returns an error.",
|
|
408
|
+
parse: stringSetting,
|
|
409
|
+
},
|
|
410
|
+
{
|
|
411
|
+
id: "pets.idleEmotes",
|
|
412
|
+
section: "pets",
|
|
413
|
+
key: "idleEmotes",
|
|
414
|
+
label: "Random idle emotes",
|
|
415
|
+
currentValue: (cfg) => String(cfg.pets.idleEmotes),
|
|
416
|
+
values: ["true", "false"],
|
|
417
|
+
description: "Occasionally flash a wave or jump while pi is idle.",
|
|
418
|
+
parse: booleanSetting,
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: "pets.idleEmoteIntervalMs",
|
|
422
|
+
section: "pets",
|
|
423
|
+
key: "idleEmoteIntervalMs",
|
|
424
|
+
label: "Idle emote interval",
|
|
425
|
+
currentValue: (cfg) => String(cfg.pets.idleEmoteIntervalMs),
|
|
426
|
+
values: ["5000", "15000", "30000", "60000", "120000", "300000"],
|
|
427
|
+
description: "Average delay between random idle pet emotes in milliseconds.",
|
|
428
|
+
parse: numberSetting,
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
id: "pets.sizeCells",
|
|
432
|
+
section: "pets",
|
|
433
|
+
key: "sizeCells",
|
|
434
|
+
label: "Size",
|
|
435
|
+
currentValue: (cfg) => String(cfg.pets.sizeCells),
|
|
436
|
+
values: ["4", "6", "8", "10", "12", "16"],
|
|
437
|
+
description: "Pet image width in terminal cells.",
|
|
438
|
+
parse: numberSetting,
|
|
439
|
+
},
|
|
440
|
+
];
|
|
441
|
+
|
|
442
|
+
export const SETTINGS_OPTION_DESCRIPTORS: readonly SettingsOptionDescriptor[] = [
|
|
443
|
+
...FAST_SETTING_DESCRIPTORS,
|
|
444
|
+
...FOOTER_SETTING_DESCRIPTORS,
|
|
445
|
+
...USAGE_SETTING_DESCRIPTORS,
|
|
446
|
+
...IMAGE_SETTING_DESCRIPTORS,
|
|
447
|
+
...LIVE_SETTING_DESCRIPTORS,
|
|
448
|
+
...PET_SETTING_DESCRIPTORS,
|
|
449
|
+
];
|
|
450
|
+
|
|
451
|
+
const SETTINGS_OPTION_BY_ID = new Map(
|
|
452
|
+
SETTINGS_OPTION_DESCRIPTORS.map((descriptor) => [descriptor.id, descriptor]),
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
456
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function configPaths(cwd: string, home = homedir(), env = process.env) {
|
|
460
|
+
return {
|
|
461
|
+
project: join(cwd, ".pi", "extensions", CONFIG_BASENAME),
|
|
462
|
+
global: join(piAgentDir(env, home), "extensions", CONFIG_BASENAME),
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
export function parseModelKey(value: string): SupportedModel | undefined {
|
|
467
|
+
const key = value.trim();
|
|
468
|
+
const slash = key.indexOf("/");
|
|
469
|
+
if (slash <= 0 || slash === key.length - 1) return undefined;
|
|
470
|
+
const provider = key.slice(0, slash).trim();
|
|
471
|
+
const id = key.slice(slash + 1).trim();
|
|
472
|
+
return provider && id ? { provider, id } : undefined;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function normalizeModelKeys(value: unknown): string[] | undefined {
|
|
476
|
+
if (value === undefined) return undefined;
|
|
477
|
+
if (!Array.isArray(value)) return undefined;
|
|
478
|
+
return value
|
|
479
|
+
.filter((entry): entry is string => typeof entry === "string")
|
|
480
|
+
.map((entry) => parseModelKey(entry))
|
|
481
|
+
.filter((entry): entry is SupportedModel => entry !== undefined)
|
|
482
|
+
.map((entry) => `${entry.provider}/${entry.id}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function parseModels(value: unknown): SupportedModel[] | undefined {
|
|
486
|
+
const keys = normalizeModelKeys(value);
|
|
487
|
+
if (keys === undefined) return undefined;
|
|
488
|
+
return keys
|
|
489
|
+
.map((key) => parseModelKey(key))
|
|
490
|
+
.filter((entry): entry is SupportedModel => entry !== undefined);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export function readRawConfig(path: string): Record<string, unknown> {
|
|
494
|
+
if (!existsSync(path)) return {};
|
|
495
|
+
try {
|
|
496
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
497
|
+
return isRecord(parsed) ? parsed : {};
|
|
498
|
+
} catch (error) {
|
|
499
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
500
|
+
console.warn(`${logPrefix()} Failed to read ${path}: ${message}`);
|
|
501
|
+
return {};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export function readConfig(path: string): ConfigFile | undefined {
|
|
506
|
+
if (!existsSync(path)) return undefined;
|
|
507
|
+
const parsed = readRawConfig(path);
|
|
508
|
+
const config: ConfigFile = {};
|
|
509
|
+
if (typeof parsed.persistState === "boolean") config.persistState = parsed.persistState;
|
|
510
|
+
if (typeof parsed.active === "boolean") config.active = parsed.active;
|
|
511
|
+
if (typeof parsed.desiredActive === "boolean") config.desiredActive = parsed.desiredActive;
|
|
512
|
+
const supportedModels = normalizeModelKeys(parsed.supportedModels);
|
|
513
|
+
if (supportedModels !== undefined) config.supportedModels = supportedModels;
|
|
514
|
+
if (isRecord(parsed.usage)) {
|
|
515
|
+
config.usage = {};
|
|
516
|
+
if (typeof parsed.usage.enabled === "boolean") config.usage.enabled = parsed.usage.enabled;
|
|
517
|
+
if (typeof parsed.usage.refreshIntervalMs === "number")
|
|
518
|
+
config.usage.refreshIntervalMs = parsed.usage.refreshIntervalMs;
|
|
519
|
+
if (typeof parsed.usage.showOnlyOnSubscriptionModels === "boolean")
|
|
520
|
+
config.usage.showOnlyOnSubscriptionModels = parsed.usage.showOnlyOnSubscriptionModels;
|
|
521
|
+
if (typeof parsed.usage.showResetTimes === "boolean")
|
|
522
|
+
config.usage.showResetTimes = parsed.usage.showResetTimes;
|
|
523
|
+
}
|
|
524
|
+
if (
|
|
525
|
+
isRecord(parsed.footer) &&
|
|
526
|
+
typeof parsed.footer.mode === "string" &&
|
|
527
|
+
(FOOTER_MODES as readonly string[]).includes(parsed.footer.mode)
|
|
528
|
+
) {
|
|
529
|
+
config.footer = { mode: parsed.footer.mode as FooterMode };
|
|
530
|
+
}
|
|
531
|
+
if (isRecord(parsed.image)) {
|
|
532
|
+
config.image = {};
|
|
533
|
+
if (typeof parsed.image.enabled === "boolean") config.image.enabled = parsed.image.enabled;
|
|
534
|
+
if (typeof parsed.image.defaultModel === "string" && parsed.image.defaultModel.trim())
|
|
535
|
+
config.image.defaultModel = parsed.image.defaultModel.trim();
|
|
536
|
+
if (
|
|
537
|
+
typeof parsed.image.defaultSave === "string" &&
|
|
538
|
+
(IMAGE_SAVE_MODES as readonly string[]).includes(parsed.image.defaultSave)
|
|
539
|
+
)
|
|
540
|
+
config.image.defaultSave = parsed.image.defaultSave as ImageSaveMode;
|
|
541
|
+
if (
|
|
542
|
+
typeof parsed.image.outputFormat === "string" &&
|
|
543
|
+
(IMAGE_OUTPUT_FORMATS as readonly string[]).includes(parsed.image.outputFormat)
|
|
544
|
+
)
|
|
545
|
+
config.image.outputFormat = parsed.image.outputFormat as ImageOutputFormat;
|
|
546
|
+
if (typeof parsed.image.timeoutMs === "number") config.image.timeoutMs = parsed.image.timeoutMs;
|
|
547
|
+
}
|
|
548
|
+
if (isRecord(parsed.live)) {
|
|
549
|
+
config.live = {};
|
|
550
|
+
if (typeof parsed.live.enabled === "boolean") config.live.enabled = parsed.live.enabled;
|
|
551
|
+
if (isLiveVoice(parsed.live.voice)) config.live.voice = parsed.live.voice;
|
|
552
|
+
}
|
|
553
|
+
if (isRecord(parsed.pets)) {
|
|
554
|
+
config.pets = {};
|
|
555
|
+
if (typeof parsed.pets.enabled === "boolean") config.pets.enabled = parsed.pets.enabled;
|
|
556
|
+
if (typeof parsed.pets.slug === "string") config.pets.slug = parsed.pets.slug.trim();
|
|
557
|
+
if (
|
|
558
|
+
typeof parsed.pets.placement === "string" &&
|
|
559
|
+
(PET_PLACEMENTS as readonly string[]).includes(parsed.pets.placement)
|
|
560
|
+
)
|
|
561
|
+
config.pets.placement = parsed.pets.placement as PetPlacement;
|
|
562
|
+
if (
|
|
563
|
+
typeof parsed.pets.state === "string" &&
|
|
564
|
+
(PET_STATES as readonly string[]).includes(parsed.pets.state)
|
|
565
|
+
)
|
|
566
|
+
config.pets.state = parsed.pets.state as PetState;
|
|
567
|
+
if (
|
|
568
|
+
typeof parsed.pets.thinkingState === "string" &&
|
|
569
|
+
(PET_STATES as readonly string[]).includes(parsed.pets.thinkingState)
|
|
570
|
+
)
|
|
571
|
+
config.pets.thinkingState = parsed.pets.thinkingState as PetState;
|
|
572
|
+
if (
|
|
573
|
+
typeof parsed.pets.toolState === "string" &&
|
|
574
|
+
(PET_STATES as readonly string[]).includes(parsed.pets.toolState)
|
|
575
|
+
)
|
|
576
|
+
config.pets.toolState = parsed.pets.toolState as PetState;
|
|
577
|
+
if (
|
|
578
|
+
typeof parsed.pets.failedToolState === "string" &&
|
|
579
|
+
(PET_STATES as readonly string[]).includes(parsed.pets.failedToolState)
|
|
580
|
+
)
|
|
581
|
+
config.pets.failedToolState = parsed.pets.failedToolState as PetState;
|
|
582
|
+
if (typeof parsed.pets.idleEmotes === "boolean")
|
|
583
|
+
config.pets.idleEmotes = parsed.pets.idleEmotes;
|
|
584
|
+
if (typeof parsed.pets.idleEmoteIntervalMs === "number")
|
|
585
|
+
config.pets.idleEmoteIntervalMs = parsed.pets.idleEmoteIntervalMs;
|
|
586
|
+
if (typeof parsed.pets.sizeCells === "number") config.pets.sizeCells = parsed.pets.sizeCells;
|
|
587
|
+
}
|
|
588
|
+
return config;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
export type SettingPatchContext = SettingsValueContext & {
|
|
592
|
+
persistState?: boolean;
|
|
593
|
+
active?: boolean;
|
|
594
|
+
desiredActive?: boolean;
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
export function applySettingToRawConfig(
|
|
598
|
+
current: Record<string, unknown>,
|
|
599
|
+
id: string,
|
|
600
|
+
rawValue: string,
|
|
601
|
+
context: SettingPatchContext = {},
|
|
602
|
+
): Record<string, unknown> {
|
|
603
|
+
const next: Record<string, unknown> = { ...current };
|
|
604
|
+
const bool = rawValue === "true";
|
|
605
|
+
if (id === "fast.enabled") {
|
|
606
|
+
if (context.persistState) {
|
|
607
|
+
next.active = context.active ?? bool;
|
|
608
|
+
next.desiredActive = context.desiredActive ?? bool;
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
const descriptor = SETTINGS_OPTION_BY_ID.get(id);
|
|
612
|
+
if (!descriptor) return next;
|
|
613
|
+
const parsedValue = descriptor.parse(rawValue, context);
|
|
614
|
+
if (descriptor.section === "root") next[descriptor.key] = parsedValue;
|
|
615
|
+
else {
|
|
616
|
+
const currentSection = next[descriptor.section];
|
|
617
|
+
const section = isRecord(currentSection) ? { ...currentSection } : {};
|
|
618
|
+
section[descriptor.key] = parsedValue;
|
|
619
|
+
next[descriptor.section] = section;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return next;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
export function writeConfig(path: string, config: ConfigFile | Record<string, unknown>): void {
|
|
626
|
+
try {
|
|
627
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
628
|
+
writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
629
|
+
} catch (error) {
|
|
630
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
631
|
+
console.warn(`${logPrefix()} Failed to write ${path}: ${message}`);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function ensureConfigFile(projectConfigPath: string, globalConfigPath: string): void {
|
|
636
|
+
if (existsSync(projectConfigPath) || existsSync(globalConfigPath)) return;
|
|
637
|
+
writeConfig(globalConfigPath, DEFAULT_CONFIG);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
export function resolveConfig(cwd: string): ResolvedConfig {
|
|
641
|
+
const paths = configPaths(cwd);
|
|
642
|
+
ensureConfigFile(paths.project, paths.global);
|
|
643
|
+
|
|
644
|
+
const projectConfigExists = existsSync(paths.project);
|
|
645
|
+
const globalConfigExists = existsSync(paths.global);
|
|
646
|
+
const globalConfig = readConfig(paths.global) ?? {};
|
|
647
|
+
const projectConfig = readConfig(paths.project) ?? {};
|
|
648
|
+
const merged = { ...DEFAULT_CONFIG, ...globalConfig, ...projectConfig };
|
|
649
|
+
const selectedPath = projectConfigExists ? paths.project : paths.global;
|
|
650
|
+
const desiredActive = merged.desiredActive ?? merged.active ?? false;
|
|
651
|
+
|
|
652
|
+
return {
|
|
653
|
+
configPath: selectedPath,
|
|
654
|
+
projectConfigPath: paths.project,
|
|
655
|
+
globalConfigPath: paths.global,
|
|
656
|
+
projectConfigExists,
|
|
657
|
+
globalConfigExists,
|
|
658
|
+
persistState: merged.persistState ?? true,
|
|
659
|
+
active: merged.active ?? desiredActive,
|
|
660
|
+
desiredActive,
|
|
661
|
+
supportedModels:
|
|
662
|
+
parseModels(merged.supportedModels) ?? parseModels(DEFAULT_SUPPORTED_MODELS) ?? [],
|
|
663
|
+
usage: {
|
|
664
|
+
...DEFAULT_USAGE_CONFIG,
|
|
665
|
+
...globalConfig.usage,
|
|
666
|
+
...projectConfig.usage,
|
|
667
|
+
refreshIntervalMs: Math.max(
|
|
668
|
+
15_000,
|
|
669
|
+
Math.min(
|
|
670
|
+
10 * 60_000,
|
|
671
|
+
projectConfig.usage?.refreshIntervalMs ??
|
|
672
|
+
globalConfig.usage?.refreshIntervalMs ??
|
|
673
|
+
DEFAULT_USAGE_CONFIG.refreshIntervalMs,
|
|
674
|
+
),
|
|
675
|
+
),
|
|
676
|
+
},
|
|
677
|
+
footer: {
|
|
678
|
+
...DEFAULT_FOOTER_CONFIG,
|
|
679
|
+
...globalConfig.footer,
|
|
680
|
+
...projectConfig.footer,
|
|
681
|
+
},
|
|
682
|
+
image: {
|
|
683
|
+
...DEFAULT_IMAGE_CONFIG,
|
|
684
|
+
...globalConfig.image,
|
|
685
|
+
...projectConfig.image,
|
|
686
|
+
timeoutMs: Math.max(
|
|
687
|
+
30_000,
|
|
688
|
+
Math.min(
|
|
689
|
+
5 * 60_000,
|
|
690
|
+
projectConfig.image?.timeoutMs ??
|
|
691
|
+
globalConfig.image?.timeoutMs ??
|
|
692
|
+
DEFAULT_IMAGE_CONFIG.timeoutMs,
|
|
693
|
+
),
|
|
694
|
+
),
|
|
695
|
+
},
|
|
696
|
+
live: {
|
|
697
|
+
...DEFAULT_LIVE_CONFIG,
|
|
698
|
+
...globalConfig.live,
|
|
699
|
+
...projectConfig.live,
|
|
700
|
+
},
|
|
701
|
+
pets: {
|
|
702
|
+
...DEFAULT_PET_CONFIG,
|
|
703
|
+
...globalConfig.pets,
|
|
704
|
+
...projectConfig.pets,
|
|
705
|
+
idleEmoteIntervalMs: Math.max(
|
|
706
|
+
5_000,
|
|
707
|
+
Math.min(
|
|
708
|
+
5 * 60_000,
|
|
709
|
+
projectConfig.pets?.idleEmoteIntervalMs ??
|
|
710
|
+
globalConfig.pets?.idleEmoteIntervalMs ??
|
|
711
|
+
DEFAULT_PET_CONFIG.idleEmoteIntervalMs,
|
|
712
|
+
),
|
|
713
|
+
),
|
|
714
|
+
sizeCells: Math.max(
|
|
715
|
+
4,
|
|
716
|
+
Math.min(
|
|
717
|
+
16,
|
|
718
|
+
projectConfig.pets?.sizeCells ??
|
|
719
|
+
globalConfig.pets?.sizeCells ??
|
|
720
|
+
DEFAULT_PET_CONFIG.sizeCells,
|
|
721
|
+
),
|
|
722
|
+
),
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
}
|