@narumitw/pi-btw 0.58.1 → 0.60.0

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/src/settings.ts CHANGED
@@ -3,11 +3,7 @@ import { constants } from "node:fs";
3
3
  import { mkdir, open, rename, rm, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, join } from "node:path";
5
5
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
6
- import {
7
- BTW_SHORTCUT_ACTIONS,
8
- type BtwKeybindingOverrides,
9
- normalizeBtwKey,
10
- } from "./keybindings.js";
6
+ import { BTW_SHORTCUT_ACTIONS, type BtwKeybindingOverrides, normalizeBtwKey } from "./keybindings.js";
11
7
  import { BTW_THINKING_LEVELS, type BtwThinkingLevel } from "./side-thread.js";
12
8
 
13
9
  export const BTW_SETTINGS_FILE = "pi-btw.json";
@@ -16,31 +12,32 @@ export const DEFAULT_REMEMBER_THINKING_LEVEL_CHANGES = true;
16
12
  const MAX_SETTINGS_BYTES = 64 * 1024;
17
13
 
18
14
  export interface BtwSettings {
19
- keybindings?: BtwKeybindingOverrides;
20
- model?: string;
21
- thinkingLevel?: BtwThinkingLevel;
22
- rememberThinkingLevelChanges?: boolean;
23
- fullscreenCopyOnSelect?: boolean;
15
+ keybindings?: BtwKeybindingOverrides;
16
+ model?: string;
17
+ thinkingLevel?: BtwThinkingLevel;
18
+ rememberThinkingLevelChanges?: boolean;
19
+ fullscreenCopyOnSelect?: boolean;
24
20
  }
25
21
 
26
22
  export type BtwSettingsLoadResult =
27
- | { kind: "missing" }
28
- | { kind: "invalid"; reason: string }
29
- | { kind: "loaded"; settings: BtwSettings };
23
+ | { kind: "missing" }
24
+ | { kind: "invalid"; reason: string }
25
+ | { kind: "loaded"; settings: BtwSettings };
30
26
 
31
27
  export interface BtwSettingsPatch {
32
- keybindings?: BtwKeybindingOverrides;
33
- thinkingLevel?: BtwThinkingLevel;
34
- rememberThinkingLevelChanges?: boolean;
35
- fullscreenCopyOnSelect?: boolean;
28
+ keybindings?: BtwKeybindingOverrides;
29
+ model?: string;
30
+ thinkingLevel?: BtwThinkingLevel;
31
+ rememberThinkingLevelChanges?: boolean;
32
+ fullscreenCopyOnSelect?: boolean;
36
33
  }
37
34
 
38
35
  export interface UpdateBtwSettingsOptions {
39
- /** Validate against the latest document inside the mutation queue, before applying the patch. */
40
- validateCurrent?: (settings: BtwSettings) => void;
41
- settingsPath?: string;
42
- signal?: AbortSignal;
43
- beforeRename?: (temporaryPath: string, settingsPath: string) => Promise<void>;
36
+ /** Validate against the latest document inside the mutation queue, before applying the patch. */
37
+ validateCurrent?: (settings: BtwSettings) => void;
38
+ settingsPath?: string;
39
+ signal?: AbortSignal;
40
+ beforeRename?: (temporaryPath: string, settingsPath: string) => Promise<void>;
44
41
  }
45
42
 
46
43
  type SettingsDocument = Record<string, unknown>;
@@ -48,258 +45,250 @@ type SettingsDocument = Record<string, unknown>;
48
45
  const mutationQueues = new Map<string, Promise<void>>();
49
46
 
50
47
  export function btwSettingsPath(): string {
51
- return join(getAgentDir(), BTW_SETTINGS_FILE);
48
+ return join(getAgentDir(), BTW_SETTINGS_FILE);
52
49
  }
53
50
 
54
51
  export function normalizeBtwSettings(value: unknown): BtwSettings | undefined {
55
- if (!isSettingsDocument(value)) return undefined;
52
+ if (!isSettingsDocument(value)) return undefined;
56
53
 
57
- const settings: BtwSettings = {};
58
- if (Object.hasOwn(value, "keybindings")) {
59
- const keys = value.keybindings;
60
- if (!isSettingsDocument(keys)) return undefined;
61
- settings.keybindings = {};
62
- for (const action of BTW_SHORTCUT_ACTIONS) {
63
- if (!Object.hasOwn(keys, action)) continue;
64
- const key = normalizeBtwKey(keys[action]);
65
- if (!key) return undefined;
66
- settings.keybindings[action] = key;
67
- }
68
- }
69
- if (Object.hasOwn(value, "model")) {
70
- const model = Reflect.get(value, "model");
71
- if (typeof model !== "string" || !parseBtwModelReference(model)) return undefined;
72
- settings.model = model;
73
- }
74
- if (Object.hasOwn(value, "thinkingLevel")) {
75
- const thinkingLevel = Reflect.get(value, "thinkingLevel");
76
- if (!isBtwThinkingLevel(thinkingLevel)) return undefined;
77
- settings.thinkingLevel = thinkingLevel;
78
- }
79
- if (Object.hasOwn(value, "rememberThinkingLevelChanges")) {
80
- const remember = Reflect.get(value, "rememberThinkingLevelChanges");
81
- if (typeof remember !== "boolean") return undefined;
82
- settings.rememberThinkingLevelChanges = remember;
83
- }
84
- if (Object.hasOwn(value, "fullscreenCopyOnSelect")) {
85
- const copyOnSelect = Reflect.get(value, "fullscreenCopyOnSelect");
86
- if (typeof copyOnSelect !== "boolean") return undefined;
87
- settings.fullscreenCopyOnSelect = copyOnSelect;
88
- }
89
- return settings;
54
+ const settings: BtwSettings = {};
55
+ if (Object.hasOwn(value, "keybindings")) {
56
+ const keys = value.keybindings;
57
+ if (!isSettingsDocument(keys)) return undefined;
58
+ settings.keybindings = {};
59
+ for (const action of BTW_SHORTCUT_ACTIONS) {
60
+ if (!Object.hasOwn(keys, action)) continue;
61
+ const key = normalizeBtwKey(keys[action]);
62
+ if (!key) return undefined;
63
+ settings.keybindings[action] = key;
64
+ }
65
+ }
66
+ if (Object.hasOwn(value, "model")) {
67
+ const model = Reflect.get(value, "model");
68
+ if (typeof model !== "string" || !parseBtwModelReference(model)) return undefined;
69
+ settings.model = model;
70
+ }
71
+ if (Object.hasOwn(value, "thinkingLevel")) {
72
+ const thinkingLevel = Reflect.get(value, "thinkingLevel");
73
+ if (!isBtwThinkingLevel(thinkingLevel)) return undefined;
74
+ settings.thinkingLevel = thinkingLevel;
75
+ }
76
+ if (Object.hasOwn(value, "rememberThinkingLevelChanges")) {
77
+ const remember = Reflect.get(value, "rememberThinkingLevelChanges");
78
+ if (typeof remember !== "boolean") return undefined;
79
+ settings.rememberThinkingLevelChanges = remember;
80
+ }
81
+ if (Object.hasOwn(value, "fullscreenCopyOnSelect")) {
82
+ const copyOnSelect = Reflect.get(value, "fullscreenCopyOnSelect");
83
+ if (typeof copyOnSelect !== "boolean") return undefined;
84
+ settings.fullscreenCopyOnSelect = copyOnSelect;
85
+ }
86
+ return settings;
90
87
  }
91
88
 
92
- export function parseBtwModelReference(
93
- reference: string,
94
- ): { provider: string; modelId: string } | undefined {
95
- if (/[\s\p{Cc}]/u.test(reference)) return undefined;
96
- const separator = reference.indexOf("/");
97
- if (separator <= 0 || separator === reference.length - 1) return undefined;
98
- return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
89
+ export function parseBtwModelReference(reference: string): { provider: string; modelId: string } | undefined {
90
+ if (/[\s\p{Cc}]/u.test(reference)) return undefined;
91
+ const separator = reference.indexOf("/");
92
+ if (separator <= 0 || separator === reference.length - 1) return undefined;
93
+ return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
99
94
  }
100
95
 
101
96
  export function effectiveFullscreenCopyOnSelect(settings: BtwSettings): boolean {
102
- return settings.fullscreenCopyOnSelect ?? DEFAULT_FULLSCREEN_COPY_ON_SELECT;
97
+ return settings.fullscreenCopyOnSelect ?? DEFAULT_FULLSCREEN_COPY_ON_SELECT;
103
98
  }
104
99
 
105
100
  export function effectiveRememberThinkingLevelChanges(settings: BtwSettings): boolean {
106
- return settings.rememberThinkingLevelChanges ?? DEFAULT_REMEMBER_THINKING_LEVEL_CHANGES;
101
+ return settings.rememberThinkingLevelChanges ?? DEFAULT_REMEMBER_THINKING_LEVEL_CHANGES;
107
102
  }
108
103
 
109
- export async function readBtwSettings(
110
- settingsPath = btwSettingsPath(),
111
- ): Promise<BtwSettingsLoadResult> {
112
- await awaitBtwSettingsWrites(settingsPath);
113
- return readBtwSettingsUncoordinated(settingsPath);
104
+ export async function readBtwSettings(settingsPath = btwSettingsPath()): Promise<BtwSettingsLoadResult> {
105
+ await awaitBtwSettingsWrites(settingsPath);
106
+ return readBtwSettingsUncoordinated(settingsPath);
114
107
  }
115
108
 
116
109
  export function updateBtwSettings(
117
- patch: BtwSettingsPatch,
118
- options: UpdateBtwSettingsOptions = {},
110
+ patch: BtwSettingsPatch,
111
+ options: UpdateBtwSettingsOptions = {},
119
112
  ): Promise<BtwSettings> {
120
- const settingsPath = options.settingsPath ?? btwSettingsPath();
121
- return enqueueMutation(settingsPath, async () => {
122
- options.signal?.throwIfAborted();
123
- const current = await readSettingsDocumentForUpdate(settingsPath);
124
- options.signal?.throwIfAborted();
125
- options.validateCurrent?.(normalizeBtwSettings(current) ?? {});
126
- const updated = applyBtwSettingsPatch(current, patch);
127
- const settings = normalizeBtwSettings(updated);
128
- if (!settings) throw invalidSettingsError(settingsPath, "invalid settings shape");
129
- await publishSettings(settingsPath, updated, options.signal, options.beforeRename);
130
- return settings;
131
- });
113
+ const settingsPath = options.settingsPath ?? btwSettingsPath();
114
+ return enqueueMutation(settingsPath, async () => {
115
+ options.signal?.throwIfAborted();
116
+ const current = await readSettingsDocumentForUpdate(settingsPath);
117
+ options.signal?.throwIfAborted();
118
+ options.validateCurrent?.(normalizeBtwSettings(current) ?? {});
119
+ const updated = applyBtwSettingsPatch(current, patch);
120
+ const settings = normalizeBtwSettings(updated);
121
+ if (!settings) throw invalidSettingsError(settingsPath, "invalid settings shape");
122
+ await publishSettings(settingsPath, updated, options.signal, options.beforeRename);
123
+ return settings;
124
+ });
132
125
  }
133
126
 
134
127
  export async function awaitBtwSettingsWrites(settingsPath = btwSettingsPath()): Promise<void> {
135
- await mutationQueues.get(settingsPath);
128
+ await mutationQueues.get(settingsPath);
136
129
  }
137
130
 
138
131
  function enqueueMutation<T>(settingsPath: string, mutation: () => Promise<T>): Promise<T> {
139
- const previous = mutationQueues.get(settingsPath) ?? Promise.resolve();
140
- const result = previous.then(mutation, mutation);
141
- const settled = result.then(
142
- () => undefined,
143
- () => undefined,
144
- );
145
- mutationQueues.set(settingsPath, settled);
146
- void settled.finally(() => {
147
- if (mutationQueues.get(settingsPath) === settled) mutationQueues.delete(settingsPath);
148
- });
149
- return result;
132
+ const previous = mutationQueues.get(settingsPath) ?? Promise.resolve();
133
+ const result = previous.then(mutation, mutation);
134
+ const settled = result.then(
135
+ () => undefined,
136
+ () => undefined,
137
+ );
138
+ mutationQueues.set(settingsPath, settled);
139
+ void settled.finally(() => {
140
+ if (mutationQueues.get(settingsPath) === settled) mutationQueues.delete(settingsPath);
141
+ });
142
+ return result;
150
143
  }
151
144
 
152
145
  async function readBtwSettingsUncoordinated(settingsPath: string): Promise<BtwSettingsLoadResult> {
153
- let contents: string;
154
- try {
155
- contents = await readSettingsContents(settingsPath);
156
- } catch (error: unknown) {
157
- if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
158
- return { kind: "invalid", reason: `${settingsPath}: ${formatError(error)}` };
159
- }
146
+ let contents: string;
147
+ try {
148
+ contents = await readSettingsContents(settingsPath);
149
+ } catch (error: unknown) {
150
+ if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
151
+ return { kind: "invalid", reason: `${settingsPath}: ${formatError(error)}` };
152
+ }
160
153
 
161
- try {
162
- const settings = normalizeBtwSettings(JSON.parse(contents) as unknown);
163
- return settings
164
- ? { kind: "loaded", settings }
165
- : { kind: "invalid", reason: `${settingsPath}: invalid settings shape` };
166
- } catch {
167
- return { kind: "invalid", reason: `${settingsPath}: invalid JSON` };
168
- }
154
+ try {
155
+ const settings = normalizeBtwSettings(JSON.parse(contents) as unknown);
156
+ return settings
157
+ ? { kind: "loaded", settings }
158
+ : { kind: "invalid", reason: `${settingsPath}: invalid settings shape` };
159
+ } catch {
160
+ return { kind: "invalid", reason: `${settingsPath}: invalid JSON` };
161
+ }
169
162
  }
170
163
 
171
164
  async function readSettingsDocumentForUpdate(settingsPath: string): Promise<SettingsDocument> {
172
- let contents: string;
173
- try {
174
- contents = await readSettingsContents(settingsPath);
175
- } catch (error: unknown) {
176
- if (isNodeError(error) && error.code === "ENOENT") return {};
177
- throw invalidSettingsError(settingsPath, formatError(error));
178
- }
165
+ let contents: string;
166
+ try {
167
+ contents = await readSettingsContents(settingsPath);
168
+ } catch (error: unknown) {
169
+ if (isNodeError(error) && error.code === "ENOENT") return {};
170
+ throw invalidSettingsError(settingsPath, formatError(error));
171
+ }
179
172
 
180
- let parsed: unknown;
181
- try {
182
- parsed = JSON.parse(contents) as unknown;
183
- } catch {
184
- throw invalidSettingsError(settingsPath, "invalid JSON");
185
- }
186
- if (!isSettingsDocument(parsed) || !normalizeBtwSettings(parsed)) {
187
- throw invalidSettingsError(settingsPath, "invalid settings shape");
188
- }
189
- return parsed;
173
+ let parsed: unknown;
174
+ try {
175
+ parsed = JSON.parse(contents) as unknown;
176
+ } catch {
177
+ throw invalidSettingsError(settingsPath, "invalid JSON");
178
+ }
179
+ if (!isSettingsDocument(parsed) || !normalizeBtwSettings(parsed)) {
180
+ throw invalidSettingsError(settingsPath, "invalid settings shape");
181
+ }
182
+ return parsed;
190
183
  }
191
184
 
192
185
  async function readSettingsContents(settingsPath: string): Promise<string> {
193
- const flags = constants.O_RDONLY | (constants.O_NONBLOCK ?? 0);
194
- const handle = await open(settingsPath, flags);
195
- try {
196
- const descriptorStats = await handle.stat();
197
- if (!descriptorStats.isFile()) throw new Error("settings path is not a regular file");
198
- if (descriptorStats.size > MAX_SETTINGS_BYTES) {
199
- throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
200
- }
186
+ const flags = constants.O_RDONLY | (constants.O_NONBLOCK ?? 0);
187
+ const handle = await open(settingsPath, flags);
188
+ try {
189
+ const descriptorStats = await handle.stat();
190
+ if (!descriptorStats.isFile()) throw new Error("settings path is not a regular file");
191
+ if (descriptorStats.size > MAX_SETTINGS_BYTES) {
192
+ throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
193
+ }
201
194
 
202
- const buffer = Buffer.alloc(MAX_SETTINGS_BYTES + 1);
203
- let offset = 0;
204
- while (offset < buffer.byteLength) {
205
- const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
206
- if (bytesRead === 0) break;
207
- offset += bytesRead;
208
- }
209
- if (offset > MAX_SETTINGS_BYTES) {
210
- throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
211
- }
212
- try {
213
- return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
214
- buffer.subarray(0, offset),
215
- );
216
- } catch {
217
- throw new Error("settings file is not valid UTF-8");
218
- }
219
- } finally {
220
- await handle.close();
221
- }
195
+ const buffer = Buffer.alloc(MAX_SETTINGS_BYTES + 1);
196
+ let offset = 0;
197
+ while (offset < buffer.byteLength) {
198
+ const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
199
+ if (bytesRead === 0) break;
200
+ offset += bytesRead;
201
+ }
202
+ if (offset > MAX_SETTINGS_BYTES) {
203
+ throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
204
+ }
205
+ try {
206
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer.subarray(0, offset));
207
+ } catch {
208
+ throw new Error("settings file is not valid UTF-8");
209
+ }
210
+ } finally {
211
+ await handle.close();
212
+ }
222
213
  }
223
214
 
224
215
  async function publishSettings(
225
- settingsPath: string,
226
- document: SettingsDocument,
227
- signal?: AbortSignal,
228
- beforeRename?: (temporaryPath: string, settingsPath: string) => Promise<void>,
216
+ settingsPath: string,
217
+ document: SettingsDocument,
218
+ signal?: AbortSignal,
219
+ beforeRename?: (temporaryPath: string, settingsPath: string) => Promise<void>,
229
220
  ): Promise<void> {
230
- signal?.throwIfAborted();
231
- const contents = `${JSON.stringify(document, null, 2)}\n`;
232
- if (Buffer.byteLength(contents, "utf8") > MAX_SETTINGS_BYTES) {
233
- throw new Error(`settings document exceeds ${MAX_SETTINGS_BYTES} bytes`);
234
- }
235
- const directory = dirname(settingsPath);
236
- await mkdir(directory, { recursive: true });
237
- signal?.throwIfAborted();
238
- const temporaryPath = join(
239
- directory,
240
- `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`,
241
- );
242
- try {
243
- await writeFile(temporaryPath, contents, {
244
- encoding: "utf8",
245
- flag: "wx",
246
- mode: 0o600,
247
- signal,
248
- });
249
- await beforeRename?.(temporaryPath, settingsPath);
250
- signal?.throwIfAborted();
251
- await rename(temporaryPath, settingsPath);
252
- } catch (error) {
253
- await rm(temporaryPath, { force: true }).catch(() => undefined);
254
- throw error;
255
- }
221
+ signal?.throwIfAborted();
222
+ const contents = `${JSON.stringify(document, null, 2)}\n`;
223
+ if (Buffer.byteLength(contents, "utf8") > MAX_SETTINGS_BYTES) {
224
+ throw new Error(`settings document exceeds ${MAX_SETTINGS_BYTES} bytes`);
225
+ }
226
+ const directory = dirname(settingsPath);
227
+ await mkdir(directory, { recursive: true });
228
+ signal?.throwIfAborted();
229
+ const temporaryPath = join(directory, `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`);
230
+ try {
231
+ await writeFile(temporaryPath, contents, {
232
+ encoding: "utf8",
233
+ flag: "wx",
234
+ mode: 0o600,
235
+ signal,
236
+ });
237
+ await beforeRename?.(temporaryPath, settingsPath);
238
+ signal?.throwIfAborted();
239
+ await rename(temporaryPath, settingsPath);
240
+ } catch (error) {
241
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
242
+ throw error;
243
+ }
256
244
  }
257
245
 
258
- function applyBtwSettingsPatch(
259
- current: SettingsDocument,
260
- patch: BtwSettingsPatch,
261
- ): SettingsDocument {
262
- const updated: SettingsDocument = { ...current };
263
- if (patch.keybindings) {
264
- const keys = isSettingsDocument(current.keybindings) ? { ...current.keybindings } : {};
265
- for (const action of BTW_SHORTCUT_ACTIONS) {
266
- if (!Object.hasOwn(patch.keybindings, action)) continue;
267
- if (patch.keybindings[action] === undefined) delete keys[action];
268
- else keys[action] = patch.keybindings[action];
269
- }
270
- if (Object.keys(keys).length) updated.keybindings = keys;
271
- else delete updated.keybindings;
272
- }
273
- if (Object.hasOwn(patch, "thinkingLevel")) {
274
- if (patch.thinkingLevel === undefined) delete updated.thinkingLevel;
275
- else updated.thinkingLevel = patch.thinkingLevel;
276
- }
277
- if (Object.hasOwn(patch, "rememberThinkingLevelChanges")) {
278
- updated.rememberThinkingLevelChanges = patch.rememberThinkingLevelChanges;
279
- }
280
- if (Object.hasOwn(patch, "fullscreenCopyOnSelect")) {
281
- if (patch.fullscreenCopyOnSelect === undefined) delete updated.fullscreenCopyOnSelect;
282
- else updated.fullscreenCopyOnSelect = patch.fullscreenCopyOnSelect;
283
- }
284
- return updated;
246
+ function applyBtwSettingsPatch(current: SettingsDocument, patch: BtwSettingsPatch): SettingsDocument {
247
+ const updated: SettingsDocument = { ...current };
248
+ if (patch.keybindings) {
249
+ const keys = isSettingsDocument(current.keybindings) ? { ...current.keybindings } : {};
250
+ for (const action of BTW_SHORTCUT_ACTIONS) {
251
+ if (!Object.hasOwn(patch.keybindings, action)) continue;
252
+ if (patch.keybindings[action] === undefined) delete keys[action];
253
+ else keys[action] = patch.keybindings[action];
254
+ }
255
+ if (Object.keys(keys).length) updated.keybindings = keys;
256
+ else delete updated.keybindings;
257
+ }
258
+ if (Object.hasOwn(patch, "model")) {
259
+ if (patch.model === undefined) delete updated.model;
260
+ else updated.model = patch.model;
261
+ }
262
+ if (Object.hasOwn(patch, "thinkingLevel")) {
263
+ if (patch.thinkingLevel === undefined) delete updated.thinkingLevel;
264
+ else updated.thinkingLevel = patch.thinkingLevel;
265
+ }
266
+ if (Object.hasOwn(patch, "rememberThinkingLevelChanges")) {
267
+ updated.rememberThinkingLevelChanges = patch.rememberThinkingLevelChanges;
268
+ }
269
+ if (Object.hasOwn(patch, "fullscreenCopyOnSelect")) {
270
+ if (patch.fullscreenCopyOnSelect === undefined) delete updated.fullscreenCopyOnSelect;
271
+ else updated.fullscreenCopyOnSelect = patch.fullscreenCopyOnSelect;
272
+ }
273
+ return updated;
285
274
  }
286
275
 
287
276
  function isSettingsDocument(value: unknown): value is SettingsDocument {
288
- return typeof value === "object" && value !== null && !Array.isArray(value);
277
+ return typeof value === "object" && value !== null && !Array.isArray(value);
289
278
  }
290
279
 
291
280
  function isBtwThinkingLevel(value: unknown): value is BtwThinkingLevel {
292
- return BTW_THINKING_LEVELS.includes(value as BtwThinkingLevel);
281
+ return BTW_THINKING_LEVELS.includes(value as BtwThinkingLevel);
293
282
  }
294
283
 
295
284
  function invalidSettingsError(settingsPath: string, reason: string): Error {
296
- return new Error(`pi-btw settings at ${settingsPath} are invalid: ${reason}`);
285
+ return new Error(`pi-btw settings at ${settingsPath} are invalid: ${reason}`);
297
286
  }
298
287
 
299
288
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
300
- return error instanceof Error && "code" in error;
289
+ return error instanceof Error && "code" in error;
301
290
  }
302
291
 
303
292
  function formatError(error: unknown): string {
304
- return error instanceof Error ? error.message : String(error);
293
+ return error instanceof Error ? error.message : String(error);
305
294
  }