@micsushi/agent-hotline 1.0.1 → 1.1.1
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/README.md +91 -13
- package/package.json +5 -1
- package/packages/backend/bin/agent-hotline.js +37 -2
- package/packages/backend/package.json +1 -1
- package/packages/backend/src/doctor.js +129 -0
- package/packages/backend/src/run-command.js +32 -1
- package/packages/backend/src/server.js +9 -9
- package/packages/backend/src/settings-store.js +14 -4
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Agent Hotline is a local Windows tray app that reads useful parts of Codex, Claude Code, and Antigravity replies aloud.
|
|
4
4
|
|
|
5
|
-
You keep using your coding tool like normal. Agent Hotline listens for finished responses through local hooks, skips code-heavy bits, and reads the useful prose through the desktop
|
|
5
|
+
You keep using your coding tool like normal. Agent Hotline listens for finished responses through local hooks, skips code-heavy bits, and reads the useful prose through the browser or desktop UI. The full reply stays in the original chat.
|
|
6
6
|
|
|
7
7
|
## What It Does
|
|
8
8
|
|
|
@@ -15,40 +15,57 @@ You keep using your coding tool like normal. Agent Hotline listens for finished
|
|
|
15
15
|
|
|
16
16
|
## Status
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Usable from npm or the Windows desktop installer.
|
|
19
19
|
|
|
20
20
|
Still missing:
|
|
21
21
|
|
|
22
|
-
- A normal packaged installer.
|
|
23
|
-
- A bundled backend sidecar for production installs.
|
|
24
22
|
- Voice input owned by Agent Hotline.
|
|
25
23
|
|
|
26
24
|
## Requirements
|
|
27
25
|
|
|
28
26
|
- Windows 10 or 11.
|
|
29
|
-
- Node.js 22 or newer.
|
|
30
|
-
-
|
|
27
|
+
- Node.js 22 or newer for the npm/npx commands.
|
|
28
|
+
- No repo clone is needed for normal install.
|
|
31
29
|
- For desktop app development: Rust plus the usual Tauri Windows prerequisites.
|
|
32
30
|
|
|
33
31
|
## Install
|
|
34
32
|
|
|
33
|
+
### Recommended: desktop app plus hook setup
|
|
34
|
+
|
|
35
|
+
1. Download and run the latest Windows installer:
|
|
36
|
+
|
|
37
|
+
```text
|
|
38
|
+
https://github.com/Micsushi/agent-hotline/releases/latest
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
2. Install the hooks and spoken instructions:
|
|
42
|
+
|
|
43
|
+
```powershell
|
|
44
|
+
npx --yes @micsushi/agent-hotline install --harness all --skill all
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
3. Restart Codex, Claude Code, or Antigravity so it reloads its hook files.
|
|
48
|
+
|
|
49
|
+
After that, open Agent Hotline from the Start Menu. The desktop app starts its own bundled backend and opens the panel. No separate `ah run` command is needed for the desktop installer.
|
|
50
|
+
|
|
35
51
|
### Terminal install from npm
|
|
36
52
|
|
|
37
|
-
Use this
|
|
53
|
+
Use this when you want the browser control panel instead of the native tray app:
|
|
38
54
|
|
|
39
55
|
```powershell
|
|
40
56
|
npx --yes @micsushi/agent-hotline install --harness all --skill all
|
|
57
|
+
npx --yes @micsushi/agent-hotline run
|
|
41
58
|
```
|
|
42
59
|
|
|
43
|
-
|
|
60
|
+
The install command downloads the Agent Hotline npm package and installs both parts:
|
|
44
61
|
|
|
45
62
|
- the hook/tool command used by Codex, Claude Code, and Antigravity
|
|
46
63
|
- the spoken-output skill or managed instructions
|
|
47
64
|
|
|
48
|
-
|
|
65
|
+
The run command starts the backend and opens the browser control panel:
|
|
49
66
|
|
|
50
|
-
```
|
|
51
|
-
|
|
67
|
+
```text
|
|
68
|
+
http://127.0.0.1:4777
|
|
52
69
|
```
|
|
53
70
|
|
|
54
71
|
For one repo only:
|
|
@@ -65,12 +82,20 @@ agent-hotline install --harness all --skill all
|
|
|
65
82
|
agent-hotline run
|
|
66
83
|
```
|
|
67
84
|
|
|
85
|
+
If `ah` or `agent-hotline` is not found after a global install, run:
|
|
86
|
+
|
|
87
|
+
```powershell
|
|
88
|
+
npx --yes @micsushi/agent-hotline doctor
|
|
89
|
+
npx --yes @micsushi/agent-hotline doctor --fix-path
|
|
90
|
+
```
|
|
91
|
+
|
|
68
92
|
Useful separate commands:
|
|
69
93
|
|
|
70
94
|
```powershell
|
|
71
95
|
npx --yes @micsushi/agent-hotline install-hooks --harness all
|
|
72
96
|
npx --yes @micsushi/agent-hotline install-skill --target all
|
|
73
97
|
npx --yes @micsushi/agent-hotline hook
|
|
98
|
+
npx --yes @micsushi/agent-hotline run --no-open
|
|
74
99
|
```
|
|
75
100
|
|
|
76
101
|
### Local checkout install
|
|
@@ -90,9 +115,9 @@ npm run install:tts
|
|
|
90
115
|
|
|
91
116
|
## What Gets Installed
|
|
92
117
|
|
|
93
|
-
The npm package includes the CLI/backend hook tool and the spoken skill/instructions. Users do not download those separately.
|
|
118
|
+
The npm package includes the CLI/backend hook tool, the browser control panel, and the spoken skill/instructions. Users do not download those separately.
|
|
94
119
|
|
|
95
|
-
The
|
|
120
|
+
The GitHub release includes the Windows desktop installer for the native tray/WebView app and a bundled backend. The installer does not yet write Codex, Claude Code, or Antigravity hook files, so run the npm setup command once after installing the desktop app.
|
|
96
121
|
|
|
97
122
|
## Run Locally
|
|
98
123
|
|
|
@@ -108,6 +133,18 @@ From `npx`, start the backend:
|
|
|
108
133
|
npx --yes @micsushi/agent-hotline run
|
|
109
134
|
```
|
|
110
135
|
|
|
136
|
+
This also opens the browser control panel:
|
|
137
|
+
|
|
138
|
+
```text
|
|
139
|
+
http://127.0.0.1:4777
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
To start only the backend:
|
|
143
|
+
|
|
144
|
+
```powershell
|
|
145
|
+
npx --yes @micsushi/agent-hotline run --no-open
|
|
146
|
+
```
|
|
147
|
+
|
|
111
148
|
From a local checkout, run it in the foreground while developing:
|
|
112
149
|
|
|
113
150
|
```powershell
|
|
@@ -142,6 +179,47 @@ From a local checkout, test the local hook:
|
|
|
142
179
|
|
|
143
180
|
The sentence should show up in the Agent Hotline queue. If the backend is not running, the hook exits quietly.
|
|
144
181
|
|
|
182
|
+
## Troubleshooting
|
|
183
|
+
|
|
184
|
+
### The desktop app says Backend unavailable
|
|
185
|
+
|
|
186
|
+
Install the latest GitHub release. The current desktop app starts its bundled backend automatically. If you are using the npm/browser version instead, run:
|
|
187
|
+
|
|
188
|
+
```powershell
|
|
189
|
+
npx --yes @micsushi/agent-hotline run
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### I ran ah run but no tray icon appeared
|
|
193
|
+
|
|
194
|
+
`ah run` starts the npm/browser version. It opens `http://127.0.0.1:4777` and does not start the native tray app. For the tray app, install and launch the GitHub `.exe`.
|
|
195
|
+
|
|
196
|
+
### ah is not recognized
|
|
197
|
+
|
|
198
|
+
Use `npx` directly, or install globally:
|
|
199
|
+
|
|
200
|
+
```powershell
|
|
201
|
+
npm install -g @micsushi/agent-hotline
|
|
202
|
+
npx --yes @micsushi/agent-hotline doctor
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
If npm's global command folder is missing from PATH on Windows:
|
|
206
|
+
|
|
207
|
+
```powershell
|
|
208
|
+
npx --yes @micsushi/agent-hotline doctor --fix-path
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Restart the terminal after changing PATH.
|
|
212
|
+
|
|
213
|
+
### Agent replies are not spoken
|
|
214
|
+
|
|
215
|
+
Make sure the hook and spoken instructions are installed, then restart the coding tool:
|
|
216
|
+
|
|
217
|
+
```powershell
|
|
218
|
+
npx --yes @micsushi/agent-hotline install --harness all --skill all
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Then say `hotline on` or `read aloud on` in Codex, Claude Code, or Antigravity.
|
|
222
|
+
|
|
145
223
|
## Checks
|
|
146
224
|
|
|
147
225
|
```powershell
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@micsushi/agent-hotline",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"description": "Local read-aloud hooks and tray app for AI coding agents.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ah": "packages/backend/bin/agent-hotline.js",
|
|
@@ -36,6 +36,8 @@
|
|
|
36
36
|
"install-hotline": "node packages/backend/bin/agent-hotline.js install",
|
|
37
37
|
"stage-web": "node scripts/stage-web.mjs",
|
|
38
38
|
"prepack": "node scripts/stage-web.mjs",
|
|
39
|
+
"build:backend-sea": "node scripts/build-backend-sea.mjs",
|
|
40
|
+
"sync-desktop-version": "node scripts/sync-desktop-version.mjs",
|
|
39
41
|
"test": "npm --prefix packages/backend test && npm --workspace @agent-hotline/desktop run test",
|
|
40
42
|
"lint": "eslint . && npm run rust:clippy",
|
|
41
43
|
"lint:fix": "eslint . --fix",
|
|
@@ -51,8 +53,10 @@
|
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|
|
53
55
|
"@eslint/js": "^10.0.1",
|
|
56
|
+
"esbuild": "^0.28.1",
|
|
54
57
|
"eslint": "^10.5.0",
|
|
55
58
|
"globals": "^17.6.0",
|
|
59
|
+
"postject": "^1.0.0-alpha.6",
|
|
56
60
|
"prettier": "^3.8.4"
|
|
57
61
|
},
|
|
58
62
|
"repository": {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
const { main: hookMain } = require("../src/hook-command");
|
|
4
|
+
const { createDoctorReport, fixWindowsUserPath, formatDoctorReport } = require("../src/doctor");
|
|
4
5
|
const { installHooks, installSkills, npxHookCommand, parseArgs } = require("../src/installer");
|
|
5
|
-
const { launchBackend } = require("../src/run-command");
|
|
6
|
+
const { launchBackend, openUrl } = require("../src/run-command");
|
|
6
7
|
|
|
7
8
|
function printHelp() {
|
|
8
9
|
console.log(`Agent Hotline
|
|
@@ -10,6 +11,8 @@ function printHelp() {
|
|
|
10
11
|
Usage:
|
|
11
12
|
agent-hotline run
|
|
12
13
|
agent-hotline hook
|
|
14
|
+
agent-hotline doctor
|
|
15
|
+
agent-hotline doctor --fix-path
|
|
13
16
|
agent-hotline install --harness codex --skill codex
|
|
14
17
|
agent-hotline install-hooks --harness all
|
|
15
18
|
agent-hotline install-skill --target all
|
|
@@ -24,6 +27,8 @@ Options:
|
|
|
24
27
|
--home <path>
|
|
25
28
|
--hook-command <command>
|
|
26
29
|
--port <port> Backend port for "run" (default: 4777)
|
|
30
|
+
--no-open Start only the backend; do not open the browser panel
|
|
31
|
+
--fix-path With "doctor", add npm global bin to the Windows user PATH
|
|
27
32
|
--use-npx-hook Write hooks that call "npx --yes @micsushi/agent-hotline hook"
|
|
28
33
|
`);
|
|
29
34
|
}
|
|
@@ -65,12 +70,42 @@ async function main(argv = process.argv.slice(2), options = {}) {
|
|
|
65
70
|
if (command === "run" || command === "start") {
|
|
66
71
|
const args = parseArgs([subcommand, ...rest].filter(Boolean));
|
|
67
72
|
const launcher = options.launchBackend || launchBackend;
|
|
73
|
+
const opener = options.openUrl || openUrl;
|
|
68
74
|
const result = launcher({ port: args.port });
|
|
69
75
|
console.log(`Agent Hotline backend started in the background on ${result.url}.`);
|
|
70
76
|
if (result.pid) {
|
|
71
77
|
console.log(`Process id: ${result.pid}`);
|
|
72
78
|
}
|
|
73
|
-
|
|
79
|
+
if (!args["no-open"]) {
|
|
80
|
+
opener(result.url);
|
|
81
|
+
console.log(`Opening the Agent Hotline browser panel: ${result.url}`);
|
|
82
|
+
} else {
|
|
83
|
+
console.log(`Open the Agent Hotline browser panel: ${result.url}`);
|
|
84
|
+
}
|
|
85
|
+
console.log("You can close this terminal; Agent Hotline will keep running.");
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (command === "doctor") {
|
|
90
|
+
const args = parseArgs([subcommand, ...rest].filter(Boolean));
|
|
91
|
+
const report = createDoctorReport();
|
|
92
|
+
console.log(formatDoctorReport(report));
|
|
93
|
+
if (args["fix-path"]) {
|
|
94
|
+
if (report.platform !== "win32") {
|
|
95
|
+
console.log("--fix-path is only supported on Windows.");
|
|
96
|
+
} else if (!report.binDir) {
|
|
97
|
+
console.log("Could not find npm global bin directory.");
|
|
98
|
+
} else if (report.onPath) {
|
|
99
|
+
console.log("npm global bin is already on PATH.");
|
|
100
|
+
} else {
|
|
101
|
+
const result = fixWindowsUserPath(report.binDir);
|
|
102
|
+
console.log(
|
|
103
|
+
result.ok
|
|
104
|
+
? "Added npm global bin to your user PATH. Restart your terminal."
|
|
105
|
+
: `Could not update PATH: ${result.error || "unknown error"}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
74
109
|
return 0;
|
|
75
110
|
}
|
|
76
111
|
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"start": "node src/server.js",
|
|
15
15
|
"test": "node --test test/*.test.js test/*.test.mjs",
|
|
16
|
-
"check": "node --check src/server.js && node --check src/settings-store.js && node --check src/speech-queue-store.js && node --check src/audio-cache-store.js && node --check src/installer.js && node --check src/hook-input-parser.js && node --check src/hook-command.js && node --check src/run-command.js && node --check src/speakable-filter.js && node --check src/mcp-server.mjs && node --check bin/agent-hotline.js && node --check bin/agent-hotline-hook.js && node --check bin/agent-hotline-mcp.js && node --check test/settings-store.test.js && node --check test/speech-queue-store.test.js && node --check test/audio-cache-store.test.js && node --check test/audio-endpoints.test.js && node --check test/installer.test.js && node --check test/run-command.test.js && node --check test/speakable-filter.test.js && node --check test/hook-input-parser.test.js && node --check test/hook-command.test.js && node --check test/server-endpoints.test.js && node --check test/mcp-server.test.mjs"
|
|
16
|
+
"check": "node --check src/server.js && node --check src/settings-store.js && node --check src/speech-queue-store.js && node --check src/audio-cache-store.js && node --check src/doctor.js && node --check src/installer.js && node --check src/hook-input-parser.js && node --check src/hook-command.js && node --check src/run-command.js && node --check src/speakable-filter.js && node --check src/mcp-server.mjs && node --check bin/agent-hotline.js && node --check bin/agent-hotline-hook.js && node --check bin/agent-hotline-mcp.js && node --check test/settings-store.test.js && node --check test/speech-queue-store.test.js && node --check test/audio-cache-store.test.js && node --check test/audio-endpoints.test.js && node --check test/doctor.test.js && node --check test/installer.test.js && node --check test/run-command.test.js && node --check test/speakable-filter.test.js && node --check test/hook-input-parser.test.js && node --check test/hook-command.test.js && node --check test/server-endpoints.test.js && node --check test/mcp-server.test.mjs"
|
|
17
17
|
},
|
|
18
18
|
"engines": {
|
|
19
19
|
"node": ">=18"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
const os = require("os");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { spawnSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
function globalBinFromPrefix(prefix, platform = process.platform) {
|
|
6
|
+
const trimmed = String(prefix || "").trim();
|
|
7
|
+
if (!trimmed) return "";
|
|
8
|
+
return platform === "win32" ? trimmed : path.join(trimmed, "bin");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function npmGlobalPrefix(options = {}) {
|
|
12
|
+
const spawnSyncImpl = options.spawnSync || spawnSync;
|
|
13
|
+
const platform = options.platform || process.platform;
|
|
14
|
+
const result = spawnSyncImpl("npm", ["config", "get", "prefix"], {
|
|
15
|
+
encoding: "utf8",
|
|
16
|
+
windowsHide: true
|
|
17
|
+
});
|
|
18
|
+
if (result.error || result.status !== 0) {
|
|
19
|
+
if (platform === "win32" && process.env.APPDATA) {
|
|
20
|
+
return path.join(process.env.APPDATA, "npm");
|
|
21
|
+
}
|
|
22
|
+
return "";
|
|
23
|
+
}
|
|
24
|
+
return String(result.stdout || "").trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function pathEntries(value = process.env.PATH || "") {
|
|
28
|
+
return String(value)
|
|
29
|
+
.split(path.delimiter)
|
|
30
|
+
.map((entry) => entry.trim())
|
|
31
|
+
.filter(Boolean);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function pathContains(dir, value = process.env.PATH || "", platform = process.platform) {
|
|
35
|
+
const normalize =
|
|
36
|
+
platform === "win32" ? (entry) => entry.toLowerCase().replace(/[\\/]+$/, "") : (entry) => entry;
|
|
37
|
+
const target = normalize(dir);
|
|
38
|
+
return pathEntries(value).some((entry) => normalize(entry) === target);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function windowsPathFixCommand(binDir) {
|
|
42
|
+
const escaped = String(binDir).replace(/'/g, "''");
|
|
43
|
+
return [
|
|
44
|
+
"$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')",
|
|
45
|
+
`$bin = '${escaped}'`,
|
|
46
|
+
"if ($null -eq $userPath) { $userPath = '' }",
|
|
47
|
+
"if (($userPath -split ';') -notcontains $bin) {",
|
|
48
|
+
" $nextPath = if ([string]::IsNullOrWhiteSpace($userPath)) { $bin } else { $userPath.TrimEnd(';') + ';' + $bin }",
|
|
49
|
+
" [Environment]::SetEnvironmentVariable('Path', $nextPath, 'User')",
|
|
50
|
+
"}",
|
|
51
|
+
"Write-Host 'Restart your terminal after updating PATH.'"
|
|
52
|
+
].join("; ");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function fixWindowsUserPath(binDir, options = {}) {
|
|
56
|
+
const spawnSyncImpl = options.spawnSync || spawnSync;
|
|
57
|
+
const result = spawnSyncImpl(
|
|
58
|
+
"powershell.exe",
|
|
59
|
+
["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", windowsPathFixCommand(binDir)],
|
|
60
|
+
{
|
|
61
|
+
encoding: "utf8",
|
|
62
|
+
windowsHide: true
|
|
63
|
+
}
|
|
64
|
+
);
|
|
65
|
+
return {
|
|
66
|
+
ok: !result.error && result.status === 0,
|
|
67
|
+
error: result.error ? result.error.message : String(result.stderr || "").trim()
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createDoctorReport(options = {}) {
|
|
72
|
+
const platform = options.platform || process.platform;
|
|
73
|
+
const prefix = options.prefix || npmGlobalPrefix(options);
|
|
74
|
+
const binDir = globalBinFromPrefix(prefix, platform);
|
|
75
|
+
const onPath = binDir
|
|
76
|
+
? pathContains(binDir, options.pathValue || process.env.PATH || "", platform)
|
|
77
|
+
: false;
|
|
78
|
+
const hasGlobalInstall = !!options.hasGlobalInstall;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
platform,
|
|
82
|
+
node: process.version,
|
|
83
|
+
prefix,
|
|
84
|
+
binDir,
|
|
85
|
+
onPath,
|
|
86
|
+
hasGlobalInstall,
|
|
87
|
+
pathFixCommand: platform === "win32" && binDir ? windowsPathFixCommand(binDir) : ""
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatDoctorReport(report) {
|
|
92
|
+
const lines = [
|
|
93
|
+
"Agent Hotline doctor",
|
|
94
|
+
`Node: ${report.node}`,
|
|
95
|
+
`npm global bin: ${report.binDir || "not found"}`,
|
|
96
|
+
`npm global bin on PATH: ${report.onPath ? "yes" : "no"}`,
|
|
97
|
+
""
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
if (!report.onPath && report.platform === "win32" && report.pathFixCommand) {
|
|
101
|
+
lines.push("To add npm global commands like ah to PATH:");
|
|
102
|
+
lines.push(
|
|
103
|
+
`powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "${report.pathFixCommand.replace(/"/g, '\\"')}"`
|
|
104
|
+
);
|
|
105
|
+
lines.push("");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lines.push("Recommended first run:");
|
|
109
|
+
lines.push(" npx --yes @micsushi/agent-hotline install --harness all --skill all");
|
|
110
|
+
lines.push(" npx --yes @micsushi/agent-hotline run");
|
|
111
|
+
|
|
112
|
+
if (report.platform === "win32") {
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push("Windows desktop installer:");
|
|
115
|
+
lines.push(" https://github.com/Micsushi/agent-hotline/releases/latest");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return lines.join(os.EOL);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = {
|
|
122
|
+
createDoctorReport,
|
|
123
|
+
fixWindowsUserPath,
|
|
124
|
+
formatDoctorReport,
|
|
125
|
+
globalBinFromPrefix,
|
|
126
|
+
npmGlobalPrefix,
|
|
127
|
+
pathContains,
|
|
128
|
+
windowsPathFixCommand
|
|
129
|
+
};
|
|
@@ -34,7 +34,38 @@ function launchBackend(options = {}) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function openUrl(url, options = {}) {
|
|
38
|
+
const spawnImpl = options.spawn || spawn;
|
|
39
|
+
const platform = options.platform || process.platform;
|
|
40
|
+
let command;
|
|
41
|
+
let args;
|
|
42
|
+
|
|
43
|
+
if (platform === "win32") {
|
|
44
|
+
command = "cmd";
|
|
45
|
+
args = ["/c", "start", "", url];
|
|
46
|
+
} else if (platform === "darwin") {
|
|
47
|
+
command = "open";
|
|
48
|
+
args = [url];
|
|
49
|
+
} else {
|
|
50
|
+
command = "xdg-open";
|
|
51
|
+
args = [url];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const child = spawnImpl(command, args, {
|
|
55
|
+
detached: true,
|
|
56
|
+
stdio: "ignore",
|
|
57
|
+
windowsHide: true
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
if (typeof child.unref === "function") {
|
|
61
|
+
child.unref();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { command, args };
|
|
65
|
+
}
|
|
66
|
+
|
|
37
67
|
module.exports = {
|
|
38
68
|
backendServerPath,
|
|
39
|
-
launchBackend
|
|
69
|
+
launchBackend,
|
|
70
|
+
openUrl
|
|
40
71
|
};
|
|
@@ -8,14 +8,13 @@ const {
|
|
|
8
8
|
TTS_ENGINES,
|
|
9
9
|
createSettingsStore
|
|
10
10
|
} = require("./settings-store");
|
|
11
|
-
const { createSpeechQueueStore } = require("./speech-queue-store");
|
|
11
|
+
const { createSpeechQueueStore, defaultDataDir } = require("./speech-queue-store");
|
|
12
12
|
const { createSpoolStore } = require("./spool-store");
|
|
13
13
|
const { createAudioCacheStore } = require("./audio-cache-store");
|
|
14
14
|
|
|
15
15
|
const PORT = Number(process.env.AGENT_HOTLINE_PORT || process.env.VOICE_QUESTION_LOOP_PORT || 4777);
|
|
16
16
|
const HOST = "127.0.0.1";
|
|
17
|
-
const
|
|
18
|
-
const DATA_DIR = path.join(ROOT, "data");
|
|
17
|
+
const DATA_DIR = process.env.AGENT_HOTLINE_DATA_DIR || defaultDataDir();
|
|
19
18
|
const QUESTIONS_FILE = process.env.QUESTION_FILE || path.join(DATA_DIR, "questions.json");
|
|
20
19
|
const REQUEST_LIMIT_BYTES = 1_000_000;
|
|
21
20
|
const AUDIO_BODY_LIMIT_BYTES = 96 * 1024 * 1024;
|
|
@@ -646,22 +645,23 @@ function page() {
|
|
|
646
645
|
}
|
|
647
646
|
|
|
648
647
|
function createServer(options = {}) {
|
|
648
|
+
const dataDir = options.dataDir || DATA_DIR;
|
|
649
649
|
const settingsStore =
|
|
650
650
|
options.settingsStore ||
|
|
651
651
|
createSettingsStore({
|
|
652
|
-
dataDir
|
|
652
|
+
dataDir,
|
|
653
653
|
settingsPath: options.settingsPath
|
|
654
654
|
});
|
|
655
655
|
const queueStore =
|
|
656
656
|
options.queueStore ||
|
|
657
657
|
createSpeechQueueStore({
|
|
658
|
-
dataDir
|
|
658
|
+
dataDir,
|
|
659
659
|
filePath: options.queuePath
|
|
660
660
|
});
|
|
661
661
|
const questionStore =
|
|
662
662
|
options.questionStore ||
|
|
663
663
|
createQuestionStore({
|
|
664
|
-
dataDir: options.questionDataDir ||
|
|
664
|
+
dataDir: options.questionDataDir || dataDir,
|
|
665
665
|
questionsFile: options.questionsFile,
|
|
666
666
|
answersFile: options.answersFile,
|
|
667
667
|
ensureFiles: options.ensureQuestionFiles
|
|
@@ -670,7 +670,7 @@ function createServer(options = {}) {
|
|
|
670
670
|
const audioCacheStore =
|
|
671
671
|
options.audioCacheStore ||
|
|
672
672
|
createAudioCacheStore({
|
|
673
|
-
dataDir
|
|
673
|
+
dataDir,
|
|
674
674
|
cacheDir: options.audioCacheDir,
|
|
675
675
|
maxBytes: options.audioMaxBytes,
|
|
676
676
|
getMaxBytes: options.audioMaxBytes
|
|
@@ -679,8 +679,7 @@ function createServer(options = {}) {
|
|
|
679
679
|
});
|
|
680
680
|
|
|
681
681
|
const spoolStore =
|
|
682
|
-
options.spoolStore ||
|
|
683
|
-
createSpoolStore({ dataDir: options.dataDir, filePath: options.spoolPath });
|
|
682
|
+
options.spoolStore || createSpoolStore({ dataDir, filePath: options.spoolPath });
|
|
684
683
|
try {
|
|
685
684
|
spoolStore.drain((item) => queueStore.enqueue(item));
|
|
686
685
|
} catch {}
|
|
@@ -1029,6 +1028,7 @@ if (require.main === module) {
|
|
|
1029
1028
|
}
|
|
1030
1029
|
|
|
1031
1030
|
module.exports = {
|
|
1031
|
+
DATA_DIR,
|
|
1032
1032
|
HOST,
|
|
1033
1033
|
PORT,
|
|
1034
1034
|
createServer,
|
|
@@ -8,6 +8,7 @@ const READ_BEHAVIORS = new Set(["manual", "auto"]);
|
|
|
8
8
|
const TTS_ENGINES = new Set(["webview", "kokoro", "kokoro-ts"]);
|
|
9
9
|
const NOTIFICATION_OPENS = new Set(["full", "mini"]);
|
|
10
10
|
const AUDIO_CACHE_LIMIT_MAX_MB = 100000;
|
|
11
|
+
const DEFAULT_RATE = 0.9;
|
|
11
12
|
|
|
12
13
|
const DEFAULT_SETTINGS = Object.freeze({
|
|
13
14
|
readBehavior: "manual",
|
|
@@ -16,7 +17,7 @@ const DEFAULT_SETTINGS = Object.freeze({
|
|
|
16
17
|
voice: "",
|
|
17
18
|
audioOutputDeviceId: "",
|
|
18
19
|
kokoroVoice: "af_heart",
|
|
19
|
-
rate:
|
|
20
|
+
rate: DEFAULT_RATE,
|
|
20
21
|
volume: 1,
|
|
21
22
|
skipRules: Object.freeze({
|
|
22
23
|
codeBlocks: true,
|
|
@@ -32,7 +33,9 @@ const DEFAULT_SETTINGS = Object.freeze({
|
|
|
32
33
|
notifyOnNewReply: false,
|
|
33
34
|
notificationOpens: "full",
|
|
34
35
|
highlightSpokenText: false,
|
|
35
|
-
audioCacheLimitMb: 1024
|
|
36
|
+
audioCacheLimitMb: 1024,
|
|
37
|
+
startupSplash: true,
|
|
38
|
+
startupJingle: true
|
|
36
39
|
});
|
|
37
40
|
|
|
38
41
|
function getDefaultDataDir(env = process.env, platform = process.platform) {
|
|
@@ -79,6 +82,11 @@ function numberInRangeOrDefault(value, fallback, min, max) {
|
|
|
79
82
|
return number >= min && number <= max ? number : fallback;
|
|
80
83
|
}
|
|
81
84
|
|
|
85
|
+
function normalizeRate(value, fallback) {
|
|
86
|
+
const rate = numberInRangeOrDefault(value, fallback, 0.1, 10);
|
|
87
|
+
return rate === 0.92 ? DEFAULT_RATE : rate;
|
|
88
|
+
}
|
|
89
|
+
|
|
82
90
|
function normalizeSettings(input) {
|
|
83
91
|
const source = isPlainObject(input) ? input : {};
|
|
84
92
|
const defaults = DEFAULT_SETTINGS;
|
|
@@ -93,7 +101,7 @@ function normalizeSettings(input) {
|
|
|
93
101
|
voice: stringOrDefault(source.voice, defaults.voice),
|
|
94
102
|
audioOutputDeviceId: stringOrDefault(source.audioOutputDeviceId, defaults.audioOutputDeviceId),
|
|
95
103
|
kokoroVoice: stringOrDefault(source.kokoroVoice, defaults.kokoroVoice),
|
|
96
|
-
rate:
|
|
104
|
+
rate: normalizeRate(source.rate, defaults.rate),
|
|
97
105
|
volume: numberInRangeOrDefault(source.volume, defaults.volume, 0, 1),
|
|
98
106
|
skipRules: {
|
|
99
107
|
codeBlocks: booleanOrDefault(sourceSkipRules.codeBlocks, defaults.skipRules.codeBlocks),
|
|
@@ -119,7 +127,9 @@ function normalizeSettings(input) {
|
|
|
119
127
|
defaults.audioCacheLimitMb,
|
|
120
128
|
10,
|
|
121
129
|
AUDIO_CACHE_LIMIT_MAX_MB
|
|
122
|
-
)
|
|
130
|
+
),
|
|
131
|
+
startupSplash: booleanOrDefault(source.startupSplash, defaults.startupSplash),
|
|
132
|
+
startupJingle: booleanOrDefault(source.startupJingle, defaults.startupJingle)
|
|
123
133
|
};
|
|
124
134
|
}
|
|
125
135
|
|