pi-smart-voice-notify 0.5.4 → 0.6.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/CHANGELOG.md +17 -7
- package/package.json +81 -81
- package/src/abortable-command.ts +9 -19
- package/src/ai-messages.ts +14 -31
- package/src/config-store.ts +20 -96
- package/src/desktop-notify.ts +178 -171
- package/src/focus-detect.ts +7 -7
- package/src/index.ts +100 -140
- package/src/linux.ts +44 -61
- package/src/logging.ts +7 -4
- package/src/notify-audio.ts +43 -58
- package/src/per-project-sound.ts +14 -34
- package/src/permission-forwarding-watcher.ts +20 -42
- package/src/reminder-playback.ts +1 -1
- package/src/shared/command.ts +22 -0
- package/src/shared/env.ts +63 -0
- package/src/shared/index.ts +47 -0
- package/src/shared/manifest.ts +56 -0
- package/src/shared/numbers.ts +79 -0
- package/src/shared/strings.ts +89 -0
- package/src/shared/tts-defaults.ts +32 -0
- package/src/sound-theme.ts +21 -37
- package/src/tts.ts +93 -139
- package/src/types/tts.ts +1 -1
- package/src/types.ts +21 -34
- package/src/webhook.ts +14 -23
- package/src/zellij-modal.ts +81 -55
package/CHANGELOG.md
CHANGED
|
@@ -2,18 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-07-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added an `enabled` master toggle to the config so the extension can be disabled without uninstalling. ([76cd96f](https://github.com/MasuRii/pi-smart-voice-notify/commit/76cd96fbbd6bd0658f00e8d7c043c44260f856bd))
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
- Widened Pi coding-agent and Pi TUI peer dependency ranges to include `^0.80.0` and bumped development dependencies (`@types/node` to `26.1.0`, `@earendil-works/pi-coding-agent` to `^0.80.3`). ([146ff6e](https://github.com/MasuRii/pi-smart-voice-notify/commit/146ff6edf87de680958e61f0e1db34e6d337c729))
|
|
12
|
+
- Extracted shared utilities and consolidated duplicated logic across config, notification, and platform modules. ([76cd96f](https://github.com/MasuRii/pi-smart-voice-notify/commit/76cd96fbbd6bd0658f00e8d7c043c44260f856bd))
|
|
13
|
+
- Extracted shared test helpers and consolidated fixtures across the test suite. ([6cf3ed0](https://github.com/MasuRii/pi-smart-voice-notify/commit/6cf3ed0168c1460ffca3c447f4a7a55565f5cade))
|
|
14
|
+
|
|
5
15
|
## [0.5.4] - 2026-06-22
|
|
6
16
|
|
|
7
17
|
### Fixed
|
|
8
|
-
- Added a command allowlist to `runAbortableCommand` so non-allowlisted executables are rejected before spawning, preventing arbitrary command injection.
|
|
9
|
-
- Replaced dynamic string-based command spawning in Linux helpers with a typed `LinuxCommandName` union and dedicated `spawnLinuxCommand` switch.
|
|
18
|
+
- Added a command allowlist to `runAbortableCommand` so non-allowlisted executables are rejected before spawning, preventing arbitrary command injection ([2074e30](https://github.com/MasuRii/pi-smart-voice-notify/commit/2074e30)).
|
|
19
|
+
- Replaced dynamic string-based command spawning in Linux helpers with a typed `LinuxCommandName` union and dedicated `spawnLinuxCommand` switch ([2074e30](https://github.com/MasuRii/pi-smart-voice-notify/commit/2074e30)).
|
|
10
20
|
|
|
11
21
|
### Changed
|
|
12
|
-
- Added `postinstall` hook that runs `patch-vulnerable-deps.mjs` when installed under `.pi/agent/extensions
|
|
13
|
-
- Pinned `protobufjs` 7.6.3, `ws` 8.21.0, and `uuid` 11.1.1 via npm `overrides` to resolve known vulnerabilities.
|
|
14
|
-
- Added `@earendil-works/pi-coding-agent` as a devDependency for type-checking.
|
|
15
|
-
- Updated README badge styling to for-the-badge, added platform badge and ko-fi support button.
|
|
16
|
-
- Added `config-store-env-override` test to the test script.
|
|
22
|
+
- Added `postinstall` hook that runs `patch-vulnerable-deps.mjs` when installed under `.pi/agent/extensions/` ([924e585](https://github.com/MasuRii/pi-smart-voice-notify/commit/924e585)).
|
|
23
|
+
- Pinned `protobufjs` 7.6.3, `ws` 8.21.0, and `uuid` 11.1.1 via npm `overrides` to resolve known vulnerabilities ([924e585](https://github.com/MasuRii/pi-smart-voice-notify/commit/924e585)).
|
|
24
|
+
- Added `@earendil-works/pi-coding-agent` as a devDependency for type-checking ([924e585](https://github.com/MasuRii/pi-smart-voice-notify/commit/924e585)).
|
|
25
|
+
- Updated README badge styling to for-the-badge, added platform badge and ko-fi support button ([f66fff6](https://github.com/MasuRii/pi-smart-voice-notify/commit/f66fff6)).
|
|
26
|
+
- Added `config-store-env-override` test to the test script ([924e585](https://github.com/MasuRii/pi-smart-voice-notify/commit/924e585)).
|
|
17
27
|
|
|
18
28
|
## [0.5.3] - 2026-06-16
|
|
19
29
|
|
package/package.json
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "pi-smart-voice-notify",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Windows-optimized smart voice, sound, and desktop notifications for Pi coding agent.",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./index.ts",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": "./index.ts"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"index.ts",
|
|
12
|
-
"src",
|
|
13
|
-
"config/config.example.json",
|
|
14
|
-
"assets",
|
|
15
|
-
"README.md",
|
|
16
|
-
"CHANGELOG.md",
|
|
17
|
-
"LICENSE"
|
|
18
|
-
],
|
|
19
|
-
"scripts": {
|
|
20
|
-
"build": "npx --yes -p typescript@5.7.3 -p @types/node@20.17.57 tsc -p tsconfig.json --noEmit",
|
|
21
|
-
"lint": "npm run build",
|
|
22
|
-
"test": "node --experimental-strip-types --test test/config-store.test.ts test/config-store-no-global.test.ts test/config-store-env-override.test.ts test/abortable-command.test.ts test/reminder-playback.test.ts test/permission-forwarding-watcher.test.ts test/sound-theme.test.ts test/webhook.test.ts test/linux.test.ts test/index.test.ts test/tts.test.ts",
|
|
23
|
-
"check": "npm run build && npm run test",
|
|
24
|
-
"postinstall": "node -e \"const fs=require('fs'),cp=require('child_process'),p=require('path');const cwd=process.cwd();const normalized=cwd.split(p.sep).join('/');if(!normalized.includes('/.pi/agent/extensions/'))process.exit(0);const s=p.resolve(cwd,'../../scripts/patch-vulnerable-deps.mjs');if(!fs.existsSync(s))process.exit(0);const r=cp.spawnSync(process.execPath,[s,'--target',cwd,'--quiet'],{stdio:'inherit'});process.exit(r.status||0)\""
|
|
25
|
-
},
|
|
26
|
-
"keywords": [
|
|
27
|
-
"pi-package",
|
|
28
|
-
"pi",
|
|
29
|
-
"pi-extension",
|
|
30
|
-
"pi-coding-agent",
|
|
31
|
-
"coding-agent",
|
|
32
|
-
"voice-notify",
|
|
33
|
-
"desktop-notification",
|
|
34
|
-
"notifications",
|
|
35
|
-
"voice",
|
|
36
|
-
"audio",
|
|
37
|
-
"text-to-speech",
|
|
38
|
-
"tts",
|
|
39
|
-
"windows",
|
|
40
|
-
"linux",
|
|
41
|
-
"macos"
|
|
42
|
-
],
|
|
43
|
-
"author": "MasuRii",
|
|
44
|
-
"license": "MIT",
|
|
45
|
-
"repository": {
|
|
46
|
-
"type": "git",
|
|
47
|
-
"url": "git+https://github.com/MasuRii/pi-smart-voice-notify.git"
|
|
48
|
-
},
|
|
49
|
-
"homepage": "https://github.com/MasuRii/pi-smart-voice-notify#readme",
|
|
50
|
-
"bugs": {
|
|
51
|
-
"url": "https://github.com/MasuRii/pi-smart-voice-notify/issues"
|
|
52
|
-
},
|
|
53
|
-
"engines": {
|
|
54
|
-
"node": ">=24"
|
|
55
|
-
},
|
|
56
|
-
"publishConfig": {
|
|
57
|
-
"access": "public"
|
|
58
|
-
},
|
|
59
|
-
"pi": {
|
|
60
|
-
"extensions": [
|
|
61
|
-
"./index.ts"
|
|
62
|
-
]
|
|
63
|
-
},
|
|
64
|
-
"peerDependencies": {
|
|
65
|
-
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0",
|
|
66
|
-
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0"
|
|
67
|
-
},
|
|
68
|
-
"dependencies": {
|
|
69
|
-
"node-notifier": "^10.0.1",
|
|
70
|
-
"undici": "^8.5.0"
|
|
71
|
-
},
|
|
72
|
-
"devDependencies": {
|
|
73
|
-
"@types/node": "
|
|
74
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
75
|
-
},
|
|
76
|
-
"overrides": {
|
|
77
|
-
"protobufjs": "7.6.3",
|
|
78
|
-
"ws": "8.21.0",
|
|
79
|
-
"uuid": "11.1.1"
|
|
80
|
-
}
|
|
81
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-smart-voice-notify",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Windows-optimized smart voice, sound, and desktop notifications for Pi coding agent.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.ts"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.ts",
|
|
12
|
+
"src",
|
|
13
|
+
"config/config.example.json",
|
|
14
|
+
"assets",
|
|
15
|
+
"README.md",
|
|
16
|
+
"CHANGELOG.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "npx --yes -p typescript@5.7.3 -p @types/node@20.17.57 tsc -p tsconfig.json --noEmit",
|
|
21
|
+
"lint": "npm run build",
|
|
22
|
+
"test": "node --experimental-strip-types --test test/config-store.test.ts test/config-store-no-global.test.ts test/config-store-env-override.test.ts test/abortable-command.test.ts test/reminder-playback.test.ts test/permission-forwarding-watcher.test.ts test/sound-theme.test.ts test/webhook.test.ts test/linux.test.ts test/index.test.ts test/tts.test.ts",
|
|
23
|
+
"check": "npm run build && npm run test",
|
|
24
|
+
"postinstall": "node -e \"const fs=require('fs'),cp=require('child_process'),p=require('path');const cwd=process.cwd();const normalized=cwd.split(p.sep).join('/');if(!normalized.includes('/.pi/agent/extensions/'))process.exit(0);const s=p.resolve(cwd,'../../scripts/patch-vulnerable-deps.mjs');if(!fs.existsSync(s))process.exit(0);const r=cp.spawnSync(process.execPath,[s,'--target',cwd,'--quiet'],{stdio:'inherit'});process.exit(r.status||0)\""
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"pi-package",
|
|
28
|
+
"pi",
|
|
29
|
+
"pi-extension",
|
|
30
|
+
"pi-coding-agent",
|
|
31
|
+
"coding-agent",
|
|
32
|
+
"voice-notify",
|
|
33
|
+
"desktop-notification",
|
|
34
|
+
"notifications",
|
|
35
|
+
"voice",
|
|
36
|
+
"audio",
|
|
37
|
+
"text-to-speech",
|
|
38
|
+
"tts",
|
|
39
|
+
"windows",
|
|
40
|
+
"linux",
|
|
41
|
+
"macos"
|
|
42
|
+
],
|
|
43
|
+
"author": "MasuRii",
|
|
44
|
+
"license": "MIT",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/MasuRii/pi-smart-voice-notify.git"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://github.com/MasuRii/pi-smart-voice-notify#readme",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/MasuRii/pi-smart-voice-notify/issues"
|
|
52
|
+
},
|
|
53
|
+
"engines": {
|
|
54
|
+
"node": ">=24"
|
|
55
|
+
},
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"pi": {
|
|
60
|
+
"extensions": [
|
|
61
|
+
"./index.ts"
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"peerDependencies": {
|
|
65
|
+
"@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0",
|
|
66
|
+
"@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"node-notifier": "^10.0.1",
|
|
70
|
+
"undici": "^8.5.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/node": "26.1.0",
|
|
74
|
+
"@earendil-works/pi-coding-agent": "^0.80.3"
|
|
75
|
+
},
|
|
76
|
+
"overrides": {
|
|
77
|
+
"protobufjs": "7.6.3",
|
|
78
|
+
"ws": "8.21.0",
|
|
79
|
+
"uuid": "11.1.1"
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/abortable-command.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
|
|
4
|
+
import { buildCommandString, attachChildHandlers } from "./shared/index.ts";
|
|
5
|
+
|
|
4
6
|
const ALLOWED_ABORTABLE_COMMANDS = new Set([
|
|
5
7
|
"aplay",
|
|
6
8
|
"edge-tts",
|
|
@@ -50,10 +52,6 @@ function stringifyError(error: unknown): string {
|
|
|
50
52
|
return String(error);
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
function buildCommandString(command: string, args: readonly string[]): string {
|
|
54
|
-
return args.length > 0 ? `${command} ${args.join(" ")}` : command;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
55
|
function stopChildProcess(child: ReturnType<typeof spawn>, force = false): void {
|
|
58
56
|
if (child.killed) {
|
|
59
57
|
return;
|
|
@@ -65,8 +63,10 @@ function stopChildProcess(child: ReturnType<typeof spawn>, force = false): void
|
|
|
65
63
|
return;
|
|
66
64
|
}
|
|
67
65
|
child.kill(force ? "SIGKILL" : "SIGTERM");
|
|
68
|
-
} catch {
|
|
69
|
-
//
|
|
66
|
+
} catch (error) {
|
|
67
|
+
// Killing a process that has already exited can throw on some platforms;
|
|
68
|
+
// the error is non-actionable for shutdown/abort flows.
|
|
69
|
+
void error;
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
@@ -99,7 +99,7 @@ export async function runAbortableCommand(
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
return await new Promise<AbortableCommandResult>((resolve) => {
|
|
102
|
-
const child = spawn(normalizedCommand,
|
|
102
|
+
const child = spawn(normalizedCommand, Array.from(args), { // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process -- normalizedCommand is checked against ALLOWED_ABORTABLE_COMMANDS (plus the current Node executable for tests) before spawn; args are passed as an array with shell disabled.
|
|
103
103
|
env: options.env ?? process.env,
|
|
104
104
|
cwd: options.cwd,
|
|
105
105
|
});
|
|
@@ -148,19 +148,9 @@ export async function runAbortableCommand(
|
|
|
148
148
|
|
|
149
149
|
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
150
150
|
|
|
151
|
-
child
|
|
152
|
-
stdout += chunk.toString();
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
child.stderr?.on("data", (chunk: Buffer | string) => {
|
|
156
|
-
stderr += chunk.toString();
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
child.on("error", (error) => {
|
|
160
|
-
spawnError = error;
|
|
161
|
-
});
|
|
151
|
+
attachChildHandlers(child, (text: string) => { stdout += text; }, (text: string) => { stderr += text; }, (error: Error) => { spawnError = error; });
|
|
162
152
|
|
|
163
|
-
child.on("close", (code) => {
|
|
153
|
+
child.on("close", (code: number | null) => {
|
|
164
154
|
cleanup();
|
|
165
155
|
resolve({
|
|
166
156
|
code: code ?? (spawnError || timedOut || aborted ? 1 : 0),
|
package/src/ai-messages.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { normalizeFloat } from "./config-store.ts";
|
|
2
2
|
import { getErrorMessage } from "./logging.ts";
|
|
3
|
+
import { clampRoundedInt } from "./shared/index.ts";
|
|
4
|
+
import type { FlatAIMessageConfig } from "./types.ts";
|
|
3
5
|
|
|
4
6
|
export const AI_EVENT_TYPES = [
|
|
5
7
|
"idle",
|
|
@@ -24,20 +26,7 @@ export interface AIMessageContext {
|
|
|
24
26
|
variables?: Record<string, string | number | boolean | null | undefined>;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
export interface AIMessageConfig {
|
|
28
|
-
enableAIMessages: boolean;
|
|
29
|
-
aiEndpoint: string;
|
|
30
|
-
aiModel: string;
|
|
31
|
-
aiApiKey: string;
|
|
32
|
-
aiTimeoutMs: number;
|
|
33
|
-
aiTemperature: number;
|
|
34
|
-
aiMaxTokens: number;
|
|
35
|
-
aiFallbackToTemplates: boolean;
|
|
36
|
-
personality: string;
|
|
37
|
-
tone: string;
|
|
38
|
-
enableMessageCache: boolean;
|
|
39
|
-
messageCacheTtlMs: number;
|
|
40
|
-
maxCacheEntries: number;
|
|
29
|
+
export interface AIMessageConfig extends FlatAIMessageConfig {
|
|
41
30
|
templates: Partial<Record<CoreAIEventType, string[]>>;
|
|
42
31
|
}
|
|
43
32
|
|
|
@@ -128,24 +117,17 @@ export const DEFAULT_AI_MESSAGE_CONFIG: AIMessageConfig = {
|
|
|
128
117
|
templates: {},
|
|
129
118
|
};
|
|
130
119
|
|
|
131
|
-
function
|
|
132
|
-
if (!Number.isFinite(value)) {
|
|
133
|
-
return fallback;
|
|
134
|
-
}
|
|
135
|
-
return Math.min(max, Math.max(min, Math.round(value)));
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function normalizeConfig(overrides: Partial<AIMessageConfig> = {}): AIMessageConfig {
|
|
120
|
+
function normalizeAIMessageConfig(overrides: Partial<AIMessageConfig> = {}): AIMessageConfig {
|
|
139
121
|
return {
|
|
140
122
|
...DEFAULT_AI_MESSAGE_CONFIG,
|
|
141
123
|
...overrides,
|
|
142
|
-
aiTimeoutMs:
|
|
124
|
+
aiTimeoutMs: clampRoundedInt(
|
|
143
125
|
overrides.aiTimeoutMs ?? DEFAULT_AI_MESSAGE_CONFIG.aiTimeoutMs,
|
|
144
126
|
DEFAULT_AI_MESSAGE_CONFIG.aiTimeoutMs,
|
|
145
127
|
1_000,
|
|
146
128
|
60_000,
|
|
147
129
|
),
|
|
148
|
-
aiMaxTokens:
|
|
130
|
+
aiMaxTokens: clampRoundedInt(
|
|
149
131
|
overrides.aiMaxTokens ?? DEFAULT_AI_MESSAGE_CONFIG.aiMaxTokens,
|
|
150
132
|
DEFAULT_AI_MESSAGE_CONFIG.aiMaxTokens,
|
|
151
133
|
40,
|
|
@@ -157,13 +139,13 @@ function normalizeConfig(overrides: Partial<AIMessageConfig> = {}): AIMessageCon
|
|
|
157
139
|
0,
|
|
158
140
|
2,
|
|
159
141
|
),
|
|
160
|
-
messageCacheTtlMs:
|
|
142
|
+
messageCacheTtlMs: clampRoundedInt(
|
|
161
143
|
overrides.messageCacheTtlMs ?? DEFAULT_AI_MESSAGE_CONFIG.messageCacheTtlMs,
|
|
162
144
|
DEFAULT_AI_MESSAGE_CONFIG.messageCacheTtlMs,
|
|
163
145
|
5_000,
|
|
164
146
|
600_000,
|
|
165
147
|
),
|
|
166
|
-
maxCacheEntries:
|
|
148
|
+
maxCacheEntries: clampRoundedInt(
|
|
167
149
|
overrides.maxCacheEntries ?? DEFAULT_AI_MESSAGE_CONFIG.maxCacheEntries,
|
|
168
150
|
DEFAULT_AI_MESSAGE_CONFIG.maxCacheEntries,
|
|
169
151
|
20,
|
|
@@ -304,15 +286,15 @@ export class AIMessageService {
|
|
|
304
286
|
private readonly debugLog: (message: string, details?: Record<string, unknown>) => void;
|
|
305
287
|
|
|
306
288
|
constructor(options: AIMessageServiceOptions = {}) {
|
|
307
|
-
this.config =
|
|
289
|
+
this.config = normalizeAIMessageConfig(options.config);
|
|
308
290
|
this.debugLog = options.debugLog ?? (() => {});
|
|
309
291
|
}
|
|
310
292
|
|
|
311
|
-
public
|
|
312
|
-
this.config =
|
|
293
|
+
public updateAIMessageConfig(overrides: Partial<AIMessageConfig>): void {
|
|
294
|
+
this.config = normalizeAIMessageConfig({ ...this.config, ...overrides });
|
|
313
295
|
}
|
|
314
296
|
|
|
315
|
-
public
|
|
297
|
+
public getAIMessageConfig(): AIMessageConfig {
|
|
316
298
|
return { ...this.config, templates: { ...this.config.templates } };
|
|
317
299
|
}
|
|
318
300
|
|
|
@@ -449,7 +431,8 @@ export class AIMessageService {
|
|
|
449
431
|
}
|
|
450
432
|
|
|
451
433
|
while (this.cache.size > this.config.maxCacheEntries) {
|
|
452
|
-
const
|
|
434
|
+
const next = this.cache.keys().next();
|
|
435
|
+
const oldestKey = typeof next.value === "string" ? next.value : undefined;
|
|
453
436
|
if (!oldestKey) {
|
|
454
437
|
break;
|
|
455
438
|
}
|
package/src/config-store.ts
CHANGED
|
@@ -2,6 +2,19 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
2
2
|
import { isAbsolute, join } from "node:path";
|
|
3
3
|
|
|
4
4
|
import { resolvePiAgentDir } from "./agent-dir.ts";
|
|
5
|
+
import {
|
|
6
|
+
clampInt,
|
|
7
|
+
clampNumber,
|
|
8
|
+
normalizeFloat,
|
|
9
|
+
normalizeStringEnum,
|
|
10
|
+
normalizeStringEnumArray,
|
|
11
|
+
parseEnvBoolean,
|
|
12
|
+
readEnvFrom,
|
|
13
|
+
toRecord,
|
|
14
|
+
ENGINE_TTS_DEFAULTS,
|
|
15
|
+
} from "./shared/index.ts";
|
|
16
|
+
|
|
17
|
+
export { clampInt, clampNumber, normalizeFloat, toRecord };
|
|
5
18
|
import type {
|
|
6
19
|
ConcreteTTSEngine,
|
|
7
20
|
MessageSet,
|
|
@@ -159,25 +172,7 @@ export const DEFAULT_CONFIG: VoiceNotifyConfig = {
|
|
|
159
172
|
sapiPitch: "medium",
|
|
160
173
|
sapiVolume: "loud",
|
|
161
174
|
|
|
162
|
-
|
|
163
|
-
edgeRate: "+10%",
|
|
164
|
-
edgePitch: "+0Hz",
|
|
165
|
-
edgeVolume: "+0%",
|
|
166
|
-
espeakVoice: "en",
|
|
167
|
-
espeakRate: 175,
|
|
168
|
-
espeakPitch: 50,
|
|
169
|
-
elevenLabsApiKey: "",
|
|
170
|
-
elevenLabsVoiceId: "cgSgspJ2msm6clMCkdW9",
|
|
171
|
-
elevenLabsModel: "eleven_turbo_v2_5",
|
|
172
|
-
elevenLabsStability: 0.5,
|
|
173
|
-
elevenLabsSimilarity: 0.75,
|
|
174
|
-
elevenLabsStyle: 0.5,
|
|
175
|
-
openaiTtsEndpoint: "",
|
|
176
|
-
openaiTtsApiKey: "",
|
|
177
|
-
openaiTtsModel: "tts-1",
|
|
178
|
-
openaiTtsVoice: "alloy",
|
|
179
|
-
openaiTtsFormat: "mp3",
|
|
180
|
-
openaiTtsSpeed: 1,
|
|
175
|
+
...ENGINE_TTS_DEFAULTS,
|
|
181
176
|
|
|
182
177
|
idleSoundFile: "assets/soft-notification.mp3",
|
|
183
178
|
permissionSoundFile: "assets/attention-alert.mp3",
|
|
@@ -261,71 +256,12 @@ export const DEFAULT_CONFIG: VoiceNotifyConfig = {
|
|
|
261
256
|
debugLog: false,
|
|
262
257
|
};
|
|
263
258
|
|
|
264
|
-
export function toRecord(value: unknown): Record<string, unknown> {
|
|
265
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
266
|
-
return {};
|
|
267
|
-
}
|
|
268
|
-
return value as Record<string, unknown>;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
259
|
function readEnv(...keys: string[]): string {
|
|
272
|
-
|
|
273
|
-
const value = process.env[key];
|
|
274
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
275
|
-
return value.trim();
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
return "";
|
|
260
|
+
return readEnvFrom(...keys);
|
|
279
261
|
}
|
|
280
262
|
|
|
281
263
|
function parseEnvBool(value: string): boolean | undefined {
|
|
282
|
-
|
|
283
|
-
return undefined;
|
|
284
|
-
}
|
|
285
|
-
const normalized = value.trim().toLowerCase();
|
|
286
|
-
if (["1", "true", "yes", "on"].includes(normalized)) {
|
|
287
|
-
return true;
|
|
288
|
-
}
|
|
289
|
-
if (["0", "false", "no", "off"].includes(normalized)) {
|
|
290
|
-
return false;
|
|
291
|
-
}
|
|
292
|
-
return undefined;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function parseNumeric(value: unknown): number | undefined {
|
|
296
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
297
|
-
return value;
|
|
298
|
-
}
|
|
299
|
-
if (typeof value === "string" && value.trim().length > 0) {
|
|
300
|
-
const parsed = Number(value.trim());
|
|
301
|
-
if (Number.isFinite(parsed)) {
|
|
302
|
-
return parsed;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return undefined;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
export function clampInt(value: unknown, fallback: number, min: number, max: number): number {
|
|
309
|
-
const numeric = parseNumeric(value);
|
|
310
|
-
if (numeric === undefined) {
|
|
311
|
-
return fallback;
|
|
312
|
-
}
|
|
313
|
-
return Math.min(max, Math.max(min, Math.trunc(numeric)));
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
export function clampNumber(value: unknown, fallback: number, min: number, max: number): number {
|
|
317
|
-
const numeric = parseNumeric(value);
|
|
318
|
-
if (numeric === undefined) {
|
|
319
|
-
return fallback;
|
|
320
|
-
}
|
|
321
|
-
return Math.min(max, Math.max(min, numeric));
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export function normalizeFloat(value: number, fallback: number, min: number, max: number): number {
|
|
325
|
-
if (!Number.isFinite(value)) {
|
|
326
|
-
return fallback;
|
|
327
|
-
}
|
|
328
|
-
return Math.min(max, Math.max(min, value));
|
|
264
|
+
return parseEnvBoolean(value);
|
|
329
265
|
}
|
|
330
266
|
|
|
331
267
|
function boolOrDefault(value: unknown, fallback: boolean): boolean {
|
|
@@ -402,17 +338,11 @@ function normalizeStringArray(value: unknown): string[] {
|
|
|
402
338
|
}
|
|
403
339
|
|
|
404
340
|
function normalizeNotificationTypes(value: unknown, fallback: NotificationType[]): NotificationType[] {
|
|
405
|
-
|
|
406
|
-
.map((entry) => entry.toLowerCase())
|
|
407
|
-
.filter((entry): entry is NotificationType => NOTIFICATION_TYPE_VALUES.includes(entry as NotificationType));
|
|
408
|
-
return candidates.length > 0 ? [...new Set(candidates)] : [...fallback];
|
|
341
|
+
return normalizeStringEnumArray(value, NOTIFICATION_TYPE_VALUES, fallback);
|
|
409
342
|
}
|
|
410
343
|
|
|
411
344
|
function normalizeFallbackChain(value: unknown, fallback: ConcreteTTSEngine[]): ConcreteTTSEngine[] {
|
|
412
|
-
|
|
413
|
-
.map((entry) => entry.toLowerCase())
|
|
414
|
-
.filter((entry): entry is ConcreteTTSEngine => CONCRETE_TTS_ENGINE_VALUES.includes(entry as ConcreteTTSEngine));
|
|
415
|
-
return candidates.length > 0 ? [...new Set(candidates)] : [...fallback];
|
|
345
|
+
return normalizeStringEnumArray(value, CONCRETE_TTS_ENGINE_VALUES, fallback);
|
|
416
346
|
}
|
|
417
347
|
|
|
418
348
|
function normalizeTemplates(value: unknown): Partial<Record<string, string[]>> {
|
|
@@ -484,17 +414,11 @@ function isHttpUrl(url: string): boolean {
|
|
|
484
414
|
}
|
|
485
415
|
|
|
486
416
|
export function normalizeMode(value: unknown): NotificationMode {
|
|
487
|
-
|
|
488
|
-
return value as NotificationMode;
|
|
489
|
-
}
|
|
490
|
-
return DEFAULT_CONFIG.notificationMode;
|
|
417
|
+
return normalizeStringEnum(value, NOTIFICATION_MODES, DEFAULT_CONFIG.notificationMode);
|
|
491
418
|
}
|
|
492
419
|
|
|
493
420
|
export function normalizeTtsEngine(value: unknown): TTSEngine {
|
|
494
|
-
|
|
495
|
-
return value as TTSEngine;
|
|
496
|
-
}
|
|
497
|
-
return DEFAULT_CONFIG.ttsEngine;
|
|
421
|
+
return normalizeStringEnum(value, TTS_ENGINE_VALUES, DEFAULT_CONFIG.ttsEngine);
|
|
498
422
|
}
|
|
499
423
|
|
|
500
424
|
export function normalizeConfig(raw: unknown): VoiceNotifyConfig {
|