walkietalkiebot 0.3.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/.claude-plugin/plugin.json +8 -0
- package/.mcp.json +8 -0
- package/README.md +169 -0
- package/bin/wtb-server.js +336 -0
- package/bin/wtb.js +43 -0
- package/dist/Talkie_logo.png +0 -0
- package/dist/assets/index-UPnYoRh1.js +81 -0
- package/dist/assets/index-VbGv60d-.css +1 -0
- package/dist/index.html +14 -0
- package/mcp-server/dist/index.js +401 -0
- package/package.json +86 -0
- package/server/api.js +629 -0
- package/server/db/index.js +67 -0
- package/server/db/repositories/activities.js +85 -0
- package/server/db/repositories/activities.test.js +106 -0
- package/server/db/repositories/conversations.js +93 -0
- package/server/db/repositories/conversations.test.js +137 -0
- package/server/db/repositories/jobs.js +128 -0
- package/server/db/repositories/messages.js +98 -0
- package/server/db/repositories/messages.test.js +152 -0
- package/server/db/repositories/plans.js +57 -0
- package/server/db/repositories/plans.test.js +98 -0
- package/server/db/repositories/search.js +34 -0
- package/server/db/repositories/search.test.js +61 -0
- package/server/db/repositories/telegram.js +30 -0
- package/server/db/schema.js +165 -0
- package/server/index.js +137 -0
- package/server/jobs/api.js +108 -0
- package/server/jobs/manager.js +231 -0
- package/server/jobs/runner.js +246 -0
- package/server/notifications/dispatcher.js +40 -0
- package/server/notifications/macos.js +24 -0
- package/server/notifications/types.js +0 -0
- package/server/ssl.js +61 -0
- package/server/state.js +30 -0
- package/server/telegram/commands.js +160 -0
- package/server/telegram/handlers.js +299 -0
- package/server/telegram/index.js +46 -0
- package/server/test/helpers.js +14 -0
- package/skills/export-tape/SKILL.md +26 -0
- package/skills/launch-voice/SKILL.md +25 -0
- package/skills/manage-plans/SKILL.md +32 -0
- package/skills/save-conversation/SKILL.md +24 -0
- package/skills/search-tapes/SKILL.md +21 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wtb",
|
|
3
|
+
"description": "Walkie Talkie Bot — voice-first cassette tape interface for Claude Code. 30 MCP tools for conversations, plans, search, voice, and more. Data tools work offline via direct SQLite.",
|
|
4
|
+
"version": "0.3.0",
|
|
5
|
+
"author": { "name": "Ben Gillin" },
|
|
6
|
+
"homepage": "https://walkietalkie.bot",
|
|
7
|
+
"repository": "https://github.com/bengillin/walkietalkiebot"
|
|
8
|
+
}
|
package/.mcp.json
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Walkie Talkie Bot
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/walkietalkiebot) [](https://www.npmjs.com/package/walkietalkiebot)
|
|
4
|
+
|
|
5
|
+
A walkie talkie for Claude. Your voice is the interface.
|
|
6
|
+
|
|
7
|
+
> **Website**: [walkietalkie.bot](https://walkietalkie.bot) -- **Docs**: [walkietalkie.bot/docs](https://walkietalkie.bot/docs/)
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
Two ways to use Walkie Talkie Bot. Pick one (or both).
|
|
12
|
+
|
|
13
|
+
### Plugin (recommended)
|
|
14
|
+
|
|
15
|
+
Add Walkie Talkie Bot as a Claude Code plugin for 30 MCP tools + 5 skills, no server required. Data tools talk directly to SQLite.
|
|
16
|
+
|
|
17
|
+
Add to `~/.claude/settings.json`:
|
|
18
|
+
|
|
19
|
+
```json
|
|
20
|
+
{
|
|
21
|
+
"mcpServers": {
|
|
22
|
+
"wtb": {
|
|
23
|
+
"command": "npx",
|
|
24
|
+
"args": ["wtb-mcp"]
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Full Install
|
|
31
|
+
|
|
32
|
+
The full server gives you the web UI, voice interface, Telegram bot, and everything.
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npx walkietalkiebot
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This starts the HTTPS server and opens `https://localhost:5173` in your browser.
|
|
39
|
+
|
|
40
|
+
> Requires Chrome or Edge for voice (Web Speech API needs secure context).
|
|
41
|
+
|
|
42
|
+
## What You Get
|
|
43
|
+
|
|
44
|
+
### Plugin
|
|
45
|
+
|
|
46
|
+
- **15 data tools** -- conversations, messages, plans, search, liner notes, export. Always work via direct SQLite, no server needed.
|
|
47
|
+
- **15 server tools** -- voice, IPC, sessions, jobs, image analysis, browser control. Require the Walkie Talkie Bot server to be running.
|
|
48
|
+
- **5 skills** -- save-conversation, search-tapes, manage-plans, launch-voice, export-tape.
|
|
49
|
+
|
|
50
|
+
### Full Install
|
|
51
|
+
|
|
52
|
+
Everything above, plus:
|
|
53
|
+
|
|
54
|
+
- Web UI with 6 retro themes and cassette tape metaphor
|
|
55
|
+
- Push-to-talk, wake word ("hey talkie"), and continuous listening
|
|
56
|
+
- Streaming text-to-speech with selectable system voices
|
|
57
|
+
- Animated robot avatar with 6 states
|
|
58
|
+
- Real-time activity feed for Claude Code tool calls
|
|
59
|
+
- Automatic plan detection and management
|
|
60
|
+
- Per-conversation liner notes
|
|
61
|
+
- Full-text search across all conversations (Cmd+K)
|
|
62
|
+
- Background job execution with SSE streaming
|
|
63
|
+
- Telegram bot integration
|
|
64
|
+
- Image drag-and-drop with Claude vision analysis
|
|
65
|
+
- Markdown and JSON export
|
|
66
|
+
|
|
67
|
+
## Themes
|
|
68
|
+
|
|
69
|
+
Six fully themed visual experiences. Every UI element reskins -- headers, chat bubbles, conversation cards, viewport borders, modals, and the robot avatar.
|
|
70
|
+
|
|
71
|
+
| Theme | Internal Name | Vibe |
|
|
72
|
+
|-------|--------------|------|
|
|
73
|
+
| **TalkBoy** | `mccallister` | Silver cassette recorder with chunky buttons and red accents |
|
|
74
|
+
| **Bubble** | `imessage` | Minimal and polished, inspired by modern Apple interfaces |
|
|
75
|
+
| **Dial-Up** | `aol` | Beveled gray panels and buddy list energy from the 90s internet |
|
|
76
|
+
| **Finder** | `classic-mac` | The elegant gray desktop of classic Mac OS (System 7/8/9) |
|
|
77
|
+
| **Guestbook** | `geocities` | Neon text on dark backgrounds, like a 90s homepage under construction |
|
|
78
|
+
| **1984** | `apple-1984` | Rainbow Apple warmth from the original Macintosh era |
|
|
79
|
+
|
|
80
|
+
Switch themes via the header button or settings drawer.
|
|
81
|
+
|
|
82
|
+
## MCP Tools
|
|
83
|
+
|
|
84
|
+
### Data Tools (offline)
|
|
85
|
+
|
|
86
|
+
Work directly against SQLite at `~/.wtb/wtb.db`. No server needed.
|
|
87
|
+
|
|
88
|
+
| Tool | Description |
|
|
89
|
+
|------|-------------|
|
|
90
|
+
| `list_conversations` | List all saved conversations with pagination |
|
|
91
|
+
| `get_conversation` | Get full conversation with messages, images, and activities |
|
|
92
|
+
| `create_conversation` | Create a new conversation (cassette tape) |
|
|
93
|
+
| `rename_conversation` | Rename an existing conversation |
|
|
94
|
+
| `delete_conversation` | Delete a conversation permanently |
|
|
95
|
+
| `search_conversations` | Full-text search (FTS5) across all conversations |
|
|
96
|
+
| `add_message` | Add a message to a conversation |
|
|
97
|
+
| `list_plans` | List all plans |
|
|
98
|
+
| `get_plan` | Get plan by ID with full content |
|
|
99
|
+
| `create_plan` | Create a plan (draft/approved/in_progress/completed/archived) |
|
|
100
|
+
| `update_plan` | Update plan title, content, or status |
|
|
101
|
+
| `delete_plan` | Delete a plan |
|
|
102
|
+
| `get_liner_notes` | Get per-conversation markdown notes |
|
|
103
|
+
| `set_liner_notes` | Set or clear liner notes |
|
|
104
|
+
| `export_conversation` | Export as markdown or JSON |
|
|
105
|
+
|
|
106
|
+
### Server Tools (requires server)
|
|
107
|
+
|
|
108
|
+
Require the Walkie Talkie Bot server to be running (`npx walkietalkiebot`).
|
|
109
|
+
|
|
110
|
+
| Tool | Description |
|
|
111
|
+
|------|-------------|
|
|
112
|
+
| `launch_wtb` | Start server and open browser |
|
|
113
|
+
| `get_wtb_status` | Check running status and avatar state |
|
|
114
|
+
| `get_transcript` | Get latest voice transcript |
|
|
115
|
+
| `get_conversation_history` | Get current tape's conversation messages |
|
|
116
|
+
| `get_claude_session` | Get connected session ID |
|
|
117
|
+
| `set_claude_session` | Connect to a Claude Code session |
|
|
118
|
+
| `disconnect_claude_session` | Disconnect session |
|
|
119
|
+
| `get_pending_message` | Poll for user messages (IPC mode) |
|
|
120
|
+
| `respond_to_wtb` | Send response to user (IPC mode) |
|
|
121
|
+
| `update_wtb_state` | Set avatar state and transcript |
|
|
122
|
+
| `analyze_image` | Analyze image via Claude vision |
|
|
123
|
+
| `open_url` | Open URL in default browser |
|
|
124
|
+
| `create_wtb_job` | Create background async job |
|
|
125
|
+
| `get_wtb_job` | Get job status and result |
|
|
126
|
+
| `list_wtb_jobs` | List jobs by status |
|
|
127
|
+
|
|
128
|
+
### Skills
|
|
129
|
+
|
|
130
|
+
| Skill | Description |
|
|
131
|
+
|-------|-------------|
|
|
132
|
+
| `save-conversation` | Save the current conversation as a cassette tape |
|
|
133
|
+
| `search-tapes` | Search across all saved tapes |
|
|
134
|
+
| `manage-plans` | Create, update, and track plans |
|
|
135
|
+
| `launch-voice` | Start the voice interface |
|
|
136
|
+
| `export-tape` | Export a conversation to markdown or JSON |
|
|
137
|
+
|
|
138
|
+
## Configuration
|
|
139
|
+
|
|
140
|
+
Set `WTB_PORT` to change the default port (5173).
|
|
141
|
+
|
|
142
|
+
**Server management:**
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
wtb-server start [-f] # Start (background or foreground)
|
|
146
|
+
wtb-server stop # Stop
|
|
147
|
+
wtb-server status # Show status
|
|
148
|
+
wtb-server logs [-f] # View logs
|
|
149
|
+
wtb-server install # Install as macOS launchd daemon
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Telegram bot:** Create a bot via [@BotFather](https://t.me/BotFather), then set `TELEGRAM_BOT_TOKEN` or save to `~/.wtb/telegram.token`. The bot starts automatically with the server.
|
|
153
|
+
|
|
154
|
+
## Development
|
|
155
|
+
|
|
156
|
+
```bash
|
|
157
|
+
npm install # Install dependencies
|
|
158
|
+
npm run dev # Vite dev server (frontend only)
|
|
159
|
+
npm run build # TypeScript check + Vite build + server bundle + MCP server
|
|
160
|
+
npm run test # Run all tests (140 tests across 10 files)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The frontend is decomposed into custom hooks (`src/hooks/`) for voice I/O, Claude chat, keyboard control, FAB dragging, image analysis, and server sync. The server has full test coverage for all database repositories and HTTP API endpoints.
|
|
164
|
+
|
|
165
|
+
Full architecture, API reference, and integration guides at [walkietalkie.bot/docs](https://walkietalkie.bot/docs/).
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
MIT
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn, execSync } from 'child_process'
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, unlinkSync, createReadStream } from 'fs'
|
|
5
|
+
import { join, dirname } from 'path'
|
|
6
|
+
import { homedir } from 'os'
|
|
7
|
+
import { fileURLToPath } from 'url'
|
|
8
|
+
import { createInterface } from 'readline'
|
|
9
|
+
|
|
10
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
11
|
+
const PLIST_NAME = 'com.wtb.server'
|
|
12
|
+
const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${PLIST_NAME}.plist`)
|
|
13
|
+
const LOG_DIR = join(homedir(), '.wtb', 'logs')
|
|
14
|
+
const PID_FILE = join(homedir(), '.wtb', 'server.pid')
|
|
15
|
+
const PORT = parseInt(process.env.WTB_PORT || '5173', 10)
|
|
16
|
+
|
|
17
|
+
function ensureDirs() {
|
|
18
|
+
const wtbDir = join(homedir(), '.wtb')
|
|
19
|
+
if (!existsSync(wtbDir)) {
|
|
20
|
+
mkdirSync(wtbDir, { recursive: true })
|
|
21
|
+
}
|
|
22
|
+
if (!existsSync(LOG_DIR)) {
|
|
23
|
+
mkdirSync(LOG_DIR, { recursive: true })
|
|
24
|
+
}
|
|
25
|
+
const launchAgentsDir = dirname(PLIST_PATH)
|
|
26
|
+
if (!existsSync(launchAgentsDir)) {
|
|
27
|
+
mkdirSync(launchAgentsDir, { recursive: true })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function generatePlist() {
|
|
32
|
+
const serverScript = join(__dirname, '..', 'server', 'index.js')
|
|
33
|
+
|
|
34
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
35
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
36
|
+
<plist version="1.0">
|
|
37
|
+
<dict>
|
|
38
|
+
<key>Label</key>
|
|
39
|
+
<string>${PLIST_NAME}</string>
|
|
40
|
+
<key>ProgramArguments</key>
|
|
41
|
+
<array>
|
|
42
|
+
<string>/usr/bin/env</string>
|
|
43
|
+
<string>node</string>
|
|
44
|
+
<string>${serverScript}</string>
|
|
45
|
+
</array>
|
|
46
|
+
<key>EnvironmentVariables</key>
|
|
47
|
+
<dict>
|
|
48
|
+
<key>PORT</key>
|
|
49
|
+
<string>${PORT}</string>
|
|
50
|
+
<key>PATH</key>
|
|
51
|
+
<string>/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin</string>
|
|
52
|
+
</dict>
|
|
53
|
+
<key>WorkingDirectory</key>
|
|
54
|
+
<string>${join(__dirname, '..')}</string>
|
|
55
|
+
<key>RunAtLoad</key>
|
|
56
|
+
<true/>
|
|
57
|
+
<key>KeepAlive</key>
|
|
58
|
+
<true/>
|
|
59
|
+
<key>StandardOutPath</key>
|
|
60
|
+
<string>${join(LOG_DIR, 'stdout.log')}</string>
|
|
61
|
+
<key>StandardErrorPath</key>
|
|
62
|
+
<string>${join(LOG_DIR, 'stderr.log')}</string>
|
|
63
|
+
</dict>
|
|
64
|
+
</plist>`
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isLaunchctlLoaded() {
|
|
68
|
+
try {
|
|
69
|
+
const result = execSync(`launchctl list ${PLIST_NAME} 2>/dev/null`, { encoding: 'utf-8' })
|
|
70
|
+
return result.includes(PLIST_NAME)
|
|
71
|
+
} catch {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isServerRunning() {
|
|
77
|
+
try {
|
|
78
|
+
execSync(`curl -s -k https://localhost:${PORT}/api/status`, { encoding: 'utf-8' })
|
|
79
|
+
return true
|
|
80
|
+
} catch {
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getPid() {
|
|
86
|
+
if (existsSync(PID_FILE)) {
|
|
87
|
+
return parseInt(readFileSync(PID_FILE, 'utf-8').trim(), 10)
|
|
88
|
+
}
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function commands() {
|
|
93
|
+
const cmd = process.argv[2]
|
|
94
|
+
const args = process.argv.slice(3)
|
|
95
|
+
|
|
96
|
+
switch (cmd) {
|
|
97
|
+
case 'start':
|
|
98
|
+
await startServer(args.includes('--foreground') || args.includes('-f'))
|
|
99
|
+
break
|
|
100
|
+
|
|
101
|
+
case 'stop':
|
|
102
|
+
await stopServer()
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
case 'restart':
|
|
106
|
+
await stopServer()
|
|
107
|
+
await startServer(false)
|
|
108
|
+
break
|
|
109
|
+
|
|
110
|
+
case 'status':
|
|
111
|
+
await showStatus()
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
case 'logs':
|
|
115
|
+
await showLogs(args.includes('--follow') || args.includes('-f'))
|
|
116
|
+
break
|
|
117
|
+
|
|
118
|
+
case 'install':
|
|
119
|
+
await installDaemon()
|
|
120
|
+
break
|
|
121
|
+
|
|
122
|
+
case 'uninstall':
|
|
123
|
+
await uninstallDaemon()
|
|
124
|
+
break
|
|
125
|
+
|
|
126
|
+
default:
|
|
127
|
+
showHelp()
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function startServer(foreground = false) {
|
|
132
|
+
ensureDirs()
|
|
133
|
+
|
|
134
|
+
if (isServerRunning()) {
|
|
135
|
+
console.log('Server is already running.')
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (foreground) {
|
|
140
|
+
console.log(`Starting Walkie Talkie Bot server in foreground on port ${PORT}...`)
|
|
141
|
+
const { startServer } = await import('../server/index.js')
|
|
142
|
+
await startServer(PORT)
|
|
143
|
+
} else {
|
|
144
|
+
// Check if launchd plist is installed
|
|
145
|
+
if (existsSync(PLIST_PATH)) {
|
|
146
|
+
console.log('Starting via launchd...')
|
|
147
|
+
execSync(`launchctl load ${PLIST_PATH}`)
|
|
148
|
+
} else {
|
|
149
|
+
// Start as background process
|
|
150
|
+
console.log('Starting as background process...')
|
|
151
|
+
const serverScript = join(__dirname, '..', 'server', 'index.js')
|
|
152
|
+
const child = spawn('node', [serverScript], {
|
|
153
|
+
detached: true,
|
|
154
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
155
|
+
env: { ...process.env, PORT: String(PORT) },
|
|
156
|
+
cwd: join(__dirname, '..'),
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
// Save PID
|
|
160
|
+
writeFileSync(PID_FILE, String(child.pid))
|
|
161
|
+
|
|
162
|
+
// Pipe logs
|
|
163
|
+
const stdout = createWriteStream(join(LOG_DIR, 'stdout.log'), { flags: 'a' })
|
|
164
|
+
const stderr = createWriteStream(join(LOG_DIR, 'stderr.log'), { flags: 'a' })
|
|
165
|
+
child.stdout?.pipe(stdout)
|
|
166
|
+
child.stderr?.pipe(stderr)
|
|
167
|
+
|
|
168
|
+
child.unref()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Wait for server to be ready
|
|
172
|
+
console.log('Waiting for server to start...')
|
|
173
|
+
for (let i = 0; i < 30; i++) {
|
|
174
|
+
await new Promise(r => setTimeout(r, 500))
|
|
175
|
+
if (isServerRunning()) {
|
|
176
|
+
console.log(`Walkie Talkie Bot server running at https://localhost:${PORT}`)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.error('Server failed to start. Check logs with: wtb-server logs')
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function stopServer() {
|
|
186
|
+
if (isLaunchctlLoaded()) {
|
|
187
|
+
console.log('Stopping via launchd...')
|
|
188
|
+
try {
|
|
189
|
+
execSync(`launchctl unload ${PLIST_PATH}`)
|
|
190
|
+
} catch {
|
|
191
|
+
// Ignore errors
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const pid = getPid()
|
|
196
|
+
if (pid) {
|
|
197
|
+
console.log(`Stopping process ${pid}...`)
|
|
198
|
+
try {
|
|
199
|
+
process.kill(pid, 'SIGTERM')
|
|
200
|
+
unlinkSync(PID_FILE)
|
|
201
|
+
} catch {
|
|
202
|
+
// Process may already be dead
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Wait for server to stop
|
|
207
|
+
for (let i = 0; i < 10; i++) {
|
|
208
|
+
await new Promise(r => setTimeout(r, 500))
|
|
209
|
+
if (!isServerRunning()) {
|
|
210
|
+
console.log('Server stopped.')
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (isServerRunning()) {
|
|
216
|
+
console.log('Warning: Server may still be running.')
|
|
217
|
+
} else {
|
|
218
|
+
console.log('Server stopped.')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function showStatus() {
|
|
223
|
+
const running = isServerRunning()
|
|
224
|
+
const launchd = isLaunchctlLoaded()
|
|
225
|
+
|
|
226
|
+
console.log('Walkie Talkie Bot Server Status')
|
|
227
|
+
console.log('================================')
|
|
228
|
+
console.log(`Server: ${running ? '✅ Running' : '❌ Stopped'}`)
|
|
229
|
+
console.log(`Port: ${PORT}`)
|
|
230
|
+
console.log(`launchd: ${launchd ? '✅ Loaded' : '⚪ Not loaded'}`)
|
|
231
|
+
console.log(`Plist: ${existsSync(PLIST_PATH) ? '✅ Installed' : '⚪ Not installed'}`)
|
|
232
|
+
|
|
233
|
+
if (running) {
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetch(`https://localhost:${PORT}/api/status`, {
|
|
236
|
+
// @ts-expect-error
|
|
237
|
+
agent: new (await import('https')).Agent({ rejectUnauthorized: false })
|
|
238
|
+
})
|
|
239
|
+
const data = await response.json()
|
|
240
|
+
console.log(`Database: ${data.dbStatus === 'connected' ? '✅ Connected' : '⚠️ Unavailable'}`)
|
|
241
|
+
console.log(`Avatar: ${data.avatarState}`)
|
|
242
|
+
} catch {
|
|
243
|
+
// Ignore
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function showLogs(follow = false) {
|
|
249
|
+
const logFile = join(LOG_DIR, 'stdout.log')
|
|
250
|
+
const errFile = join(LOG_DIR, 'stderr.log')
|
|
251
|
+
|
|
252
|
+
if (!existsSync(logFile) && !existsSync(errFile)) {
|
|
253
|
+
console.log('No logs found.')
|
|
254
|
+
return
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (follow) {
|
|
258
|
+
console.log('Following logs (Ctrl+C to stop)...\n')
|
|
259
|
+
const tail = spawn('tail', ['-f', logFile, errFile])
|
|
260
|
+
tail.stdout.pipe(process.stdout)
|
|
261
|
+
tail.stderr.pipe(process.stderr)
|
|
262
|
+
|
|
263
|
+
process.on('SIGINT', () => {
|
|
264
|
+
tail.kill()
|
|
265
|
+
process.exit(0)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
await new Promise(() => {}) // Wait forever
|
|
269
|
+
} else {
|
|
270
|
+
// Show last 50 lines
|
|
271
|
+
if (existsSync(logFile)) {
|
|
272
|
+
console.log('=== stdout.log ===')
|
|
273
|
+
const stdout = readFileSync(logFile, 'utf-8').split('\n').slice(-50).join('\n')
|
|
274
|
+
console.log(stdout)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (existsSync(errFile)) {
|
|
278
|
+
console.log('\n=== stderr.log ===')
|
|
279
|
+
const stderr = readFileSync(errFile, 'utf-8').split('\n').slice(-50).join('\n')
|
|
280
|
+
console.log(stderr)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
async function installDaemon() {
|
|
286
|
+
ensureDirs()
|
|
287
|
+
|
|
288
|
+
const plist = generatePlist()
|
|
289
|
+
writeFileSync(PLIST_PATH, plist)
|
|
290
|
+
console.log(`Installed plist to ${PLIST_PATH}`)
|
|
291
|
+
|
|
292
|
+
// Load the daemon
|
|
293
|
+
execSync(`launchctl load ${PLIST_PATH}`)
|
|
294
|
+
console.log('Daemon loaded and started.')
|
|
295
|
+
console.log(`Server will start automatically on login.`)
|
|
296
|
+
console.log(`\nTo uninstall: wtb-server uninstall`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function uninstallDaemon() {
|
|
300
|
+
if (isLaunchctlLoaded()) {
|
|
301
|
+
execSync(`launchctl unload ${PLIST_PATH}`)
|
|
302
|
+
console.log('Daemon unloaded.')
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (existsSync(PLIST_PATH)) {
|
|
306
|
+
unlinkSync(PLIST_PATH)
|
|
307
|
+
console.log(`Removed ${PLIST_PATH}`)
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
console.log('Daemon uninstalled.')
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function showHelp() {
|
|
314
|
+
console.log(`
|
|
315
|
+
Walkie Talkie Bot Server CLI
|
|
316
|
+
|
|
317
|
+
Usage: wtb-server <command> [options]
|
|
318
|
+
|
|
319
|
+
Commands:
|
|
320
|
+
start [-f] Start the server (use -f for foreground)
|
|
321
|
+
stop Stop the server
|
|
322
|
+
restart Restart the server
|
|
323
|
+
status Show server status
|
|
324
|
+
logs [-f] Show logs (use -f to follow)
|
|
325
|
+
install Install as launchd daemon (auto-start on login)
|
|
326
|
+
uninstall Remove launchd daemon
|
|
327
|
+
|
|
328
|
+
Environment:
|
|
329
|
+
WTB_PORT Server port (default: 5173)
|
|
330
|
+
`)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Import createWriteStream for logging
|
|
334
|
+
import { createWriteStream } from 'fs'
|
|
335
|
+
|
|
336
|
+
commands().catch(console.error)
|
package/bin/wtb.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { startServer } from '../server/index.js'
|
|
4
|
+
import open from 'open'
|
|
5
|
+
|
|
6
|
+
const PORT = parseInt(process.env.WTB_PORT || '5173', 10)
|
|
7
|
+
const URL = `https://localhost:${PORT}`
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
console.log('Starting Walkie Talkie Bot...')
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
await startServer(PORT)
|
|
14
|
+
|
|
15
|
+
// Open browser after server starts (prefer Chrome for Web Speech API support)
|
|
16
|
+
console.log(`Opening ${URL}...`)
|
|
17
|
+
console.log('(Requires Chrome/Edge - Firefox does not support Web Speech API)')
|
|
18
|
+
|
|
19
|
+
// Try to open in Chrome, fall back to default browser
|
|
20
|
+
try {
|
|
21
|
+
await open(URL, { app: { name: 'google chrome' } })
|
|
22
|
+
} catch {
|
|
23
|
+
await open(URL)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
console.log('\nWalkie Talkie Bot is running. Press Ctrl+C to stop.')
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err instanceof Error && err.message.includes('already in use')) {
|
|
29
|
+
console.log(`Walkie Talkie Bot appears to already be running on port ${PORT}`)
|
|
30
|
+
console.log(`Opening ${URL}...`)
|
|
31
|
+
try {
|
|
32
|
+
await open(URL, { app: { name: 'google chrome' } })
|
|
33
|
+
} catch {
|
|
34
|
+
await open(URL)
|
|
35
|
+
}
|
|
36
|
+
} else {
|
|
37
|
+
console.error('Failed to start Walkie Talkie Bot:', err)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main()
|
|
Binary file
|