talkiebot 0.1.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/README.md +263 -0
- package/bin/talkie-server.js +336 -0
- package/bin/talkie.js +43 -0
- package/dist/Talkie_logo.png +0 -0
- package/dist/assets/index-C-Y2BbXt.css +1 -0
- package/dist/assets/index-JS88FEbt.js +81 -0
- package/dist/index.html +14 -0
- package/mcp-server/index.js +694 -0
- package/package.json +70 -0
- package/server/api.js +614 -0
- package/server/db/index.js +57 -0
- package/server/db/repositories/activities.js +85 -0
- package/server/db/repositories/conversations.js +93 -0
- package/server/db/repositories/jobs.js +128 -0
- package/server/db/repositories/messages.js +98 -0
- package/server/db/repositories/plans.js +57 -0
- package/server/db/repositories/search.js +34 -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 +58 -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/README.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# Talkie
|
|
2
|
+
|
|
3
|
+
A voice-first interface for Claude Code, styled after the classic Talkie cassette recorder from Home Alone 2.
|
|
4
|
+
|
|
5
|
+
Talk to Claude with push-to-talk, wake words, or continuous listening. Manage conversations as cassette tapes. Works with web UI, Telegram bot, and MCP integration.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx talkie
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This starts the HTTPS server and opens https://localhost:5173 in your browser.
|
|
14
|
+
|
|
15
|
+
> Requires Chrome or Edge (Web Speech API).
|
|
16
|
+
|
|
17
|
+
### Setup
|
|
18
|
+
|
|
19
|
+
1. Complete the onboarding flow to configure voice settings
|
|
20
|
+
2. Choose your preferences:
|
|
21
|
+
- **Wake word**: Enable "hey talkie" hands-free activation
|
|
22
|
+
- **Continuous listening**: Always-on with trigger word
|
|
23
|
+
- **Text-to-speech**: Have responses read aloud
|
|
24
|
+
3. Start talking!
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
### Voice Input
|
|
29
|
+
|
|
30
|
+
- **Push-to-talk**: Hold spacebar or click the record button
|
|
31
|
+
- **Wake word**: Say "hey talkie" (customizable) for hands-free activation
|
|
32
|
+
- **Continuous listening**: Always-on mode that waits for your trigger word
|
|
33
|
+
- **Trigger word**: Say "over" (customizable) to end your turn
|
|
34
|
+
- **Silence detection**: Configurable delay (0.5-3.0s) after trigger word
|
|
35
|
+
- **Streaming TTS**: Responses spoken back in real-time
|
|
36
|
+
|
|
37
|
+
### Cassette Tape UI
|
|
38
|
+
|
|
39
|
+
Conversations are "tapes" in a tape deck:
|
|
40
|
+
- **Tape Deck**: Input bar with mini cassette display showing current conversation
|
|
41
|
+
- **Tape Collection**: Drawer with all conversations, eject button to browse
|
|
42
|
+
- **Switch tapes**: Click any tape to load that conversation
|
|
43
|
+
- **New tape**: Create a fresh conversation
|
|
44
|
+
|
|
45
|
+
### Claude Integration
|
|
46
|
+
|
|
47
|
+
Two modes:
|
|
48
|
+
|
|
49
|
+
- **Claude Code mode** (default): Spawns `claude -p` with full tool access. Shows real-time activity feed.
|
|
50
|
+
- **Direct API mode**: Uses your Anthropic API key. Supports streaming TTS.
|
|
51
|
+
|
|
52
|
+
### Image Handling
|
|
53
|
+
|
|
54
|
+
- Drag and drop images onto the chat for Claude vision analysis
|
|
55
|
+
- Media library to browse images across all conversations
|
|
56
|
+
- Image lightbox viewer
|
|
57
|
+
|
|
58
|
+
### Activity Feed
|
|
59
|
+
|
|
60
|
+
When Claude Code runs tools, the UI shows tool name, icon, input details, and live status (spinner, checkmark, error). Collapsible per-message activity history persisted to SQLite.
|
|
61
|
+
|
|
62
|
+
### Mobile Support
|
|
63
|
+
|
|
64
|
+
- Floating Action Button (FAB) for recording — draggable and resizable
|
|
65
|
+
- Mobile dropdown menu for settings, media library, tape collection
|
|
66
|
+
|
|
67
|
+
### Keyboard Shortcuts
|
|
68
|
+
|
|
69
|
+
| Key | Action |
|
|
70
|
+
|-----|--------|
|
|
71
|
+
| `Spacebar` (hold) | Push-to-talk (when not in continuous mode) |
|
|
72
|
+
| `Escape` | Cancel recording |
|
|
73
|
+
| `Cmd/Ctrl+K` | Search |
|
|
74
|
+
| `Cmd/Ctrl+E` | Export conversation |
|
|
75
|
+
|
|
76
|
+
### Export
|
|
77
|
+
|
|
78
|
+
Export conversations as Markdown, JSON, or plain text.
|
|
79
|
+
|
|
80
|
+
## Server Management
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
talkie-server start [-f] # Start (background, or -f for foreground)
|
|
84
|
+
talkie-server stop # Stop the server
|
|
85
|
+
talkie-server restart # Restart
|
|
86
|
+
talkie-server status # Show status (running, port, launchd, DB)
|
|
87
|
+
talkie-server logs [-f] # View logs (-f to follow)
|
|
88
|
+
talkie-server install # Install as macOS launchd daemon (auto-start on login)
|
|
89
|
+
talkie-server uninstall # Remove launchd daemon
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Set `TALKIE_PORT` to change the default port (5173).
|
|
93
|
+
|
|
94
|
+
## Telegram Bot
|
|
95
|
+
|
|
96
|
+
Chat with Claude from your phone via Telegram.
|
|
97
|
+
|
|
98
|
+
### Setup
|
|
99
|
+
|
|
100
|
+
1. Create a bot via [@BotFather](https://t.me/BotFather)
|
|
101
|
+
2. Provide the token:
|
|
102
|
+
- Environment variable: `TELEGRAM_BOT_TOKEN=your-token`
|
|
103
|
+
- Or token file: `~/.talkie/telegram.token`
|
|
104
|
+
3. Start the server — bot starts automatically
|
|
105
|
+
|
|
106
|
+
### Commands
|
|
107
|
+
|
|
108
|
+
| Command | Description |
|
|
109
|
+
|---------|-------------|
|
|
110
|
+
| `/start` | Welcome message |
|
|
111
|
+
| `/help` | Show all commands |
|
|
112
|
+
| `/conversations` | List recent conversations with selection buttons |
|
|
113
|
+
| `/new <name>` | Create a new conversation |
|
|
114
|
+
| `/current` | Show current conversation info |
|
|
115
|
+
| `/status` | Check server and Claude status |
|
|
116
|
+
|
|
117
|
+
Send text messages to chat. Send photos (with optional captions) for image analysis.
|
|
118
|
+
|
|
119
|
+
## MCP Integration
|
|
120
|
+
|
|
121
|
+
Claude Code can launch and interact with Talkie via MCP tools.
|
|
122
|
+
|
|
123
|
+
### Setup
|
|
124
|
+
|
|
125
|
+
Add to `~/.claude/settings.json`:
|
|
126
|
+
|
|
127
|
+
```json
|
|
128
|
+
{
|
|
129
|
+
"mcpServers": {
|
|
130
|
+
"talkie": {
|
|
131
|
+
"command": "npx",
|
|
132
|
+
"args": ["talkie-mcp"]
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Tools
|
|
139
|
+
|
|
140
|
+
| Tool | Description |
|
|
141
|
+
|------|-------------|
|
|
142
|
+
| `launch_talkie` | Start server and open browser |
|
|
143
|
+
| `get_talkie_status` | Check running status and avatar state |
|
|
144
|
+
| `get_transcript` | Get latest voice transcript |
|
|
145
|
+
| `get_conversation_history` | Get current conversation messages |
|
|
146
|
+
| `get_claude_session` | Get connected session ID |
|
|
147
|
+
| `set_claude_session` | Connect to a Claude Code session |
|
|
148
|
+
| `disconnect_claude_session` | Disconnect session |
|
|
149
|
+
| `get_pending_message` | Poll for user messages (IPC mode) |
|
|
150
|
+
| `respond_to_talkie` | Send response to user (IPC mode) |
|
|
151
|
+
| `update_talkie_state` | Set avatar state, transcript |
|
|
152
|
+
| `analyze_image` | Analyze image via Claude vision |
|
|
153
|
+
| `open_url` | Open URL in default browser |
|
|
154
|
+
|
|
155
|
+
## Persistence
|
|
156
|
+
|
|
157
|
+
- **Browser**: localStorage for settings and conversation cache
|
|
158
|
+
- **Server**: SQLite at `~/.talkie/talkie.db` (WAL mode, FTS5 full-text search)
|
|
159
|
+
- **Migration**: On first server connection, localStorage data auto-migrates to SQLite
|
|
160
|
+
|
|
161
|
+
## Architecture
|
|
162
|
+
|
|
163
|
+
```
|
|
164
|
+
Browser (React 18 + TypeScript + Zustand)
|
|
165
|
+
│
|
|
166
|
+
├── Voice: Web Speech API (STT/TTS)
|
|
167
|
+
├── State: Zustand + localStorage cache
|
|
168
|
+
└── API ──► HTTPS Server (Hono)
|
|
169
|
+
│
|
|
170
|
+
├── SQLite (conversations, messages, activities, images)
|
|
171
|
+
├── Claude Code CLI (spawn per message)
|
|
172
|
+
├── Telegram Bot (grammy)
|
|
173
|
+
└── Static files (dist/)
|
|
174
|
+
|
|
175
|
+
MCP Server (stdio) ──► HTTPS Server API
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
```
|
|
179
|
+
src/
|
|
180
|
+
├── App.tsx Main orchestration
|
|
181
|
+
├── components/
|
|
182
|
+
│ ├── activity/ Real-time tool usage feed
|
|
183
|
+
│ ├── avatar/ Animated avatar with states
|
|
184
|
+
│ ├── cassette/ Tape deck, collection, visuals
|
|
185
|
+
│ ├── chat/ Timeline, sidebar, input bar
|
|
186
|
+
│ ├── dropzone/ Image drag-and-drop
|
|
187
|
+
│ ├── media/ Lightbox and library
|
|
188
|
+
│ ├── onboarding/ First-run setup
|
|
189
|
+
│ ├── settings/ Settings drawer
|
|
190
|
+
│ └── voice/ Recognition, synthesis, wake word
|
|
191
|
+
├── lib/
|
|
192
|
+
│ ├── claude.ts Claude API (direct + CLI + vision)
|
|
193
|
+
│ ├── store.ts Zustand state
|
|
194
|
+
│ └── api.ts Server API client
|
|
195
|
+
├── hooks/ Keyboard shortcuts, sound effects
|
|
196
|
+
└── contexts/ Theme context
|
|
197
|
+
|
|
198
|
+
server/
|
|
199
|
+
├── index.ts Server startup (HTTPS, DB, Telegram)
|
|
200
|
+
├── api.ts All API routes
|
|
201
|
+
├── state.ts In-memory IPC state
|
|
202
|
+
├── ssl.ts Self-signed cert generation
|
|
203
|
+
├── db/
|
|
204
|
+
│ ├── schema.ts Versioned migrations
|
|
205
|
+
│ └── repositories/ CRUD modules
|
|
206
|
+
└── telegram/ Bot commands and handlers
|
|
207
|
+
|
|
208
|
+
mcp-server/index.js MCP server (12 tools, stdio)
|
|
209
|
+
bin/
|
|
210
|
+
├── talkie.js Start server + open browser
|
|
211
|
+
├── talkie-server.js Server lifecycle CLI
|
|
212
|
+
└── talkie-mcp.js MCP server entry
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### API Endpoints
|
|
216
|
+
|
|
217
|
+
| Endpoint | Method | Description |
|
|
218
|
+
|----------|--------|-------------|
|
|
219
|
+
| `/api/status` | GET | Health check, avatar state, DB status |
|
|
220
|
+
| `/api/transcript` | GET | Latest voice transcript and messages |
|
|
221
|
+
| `/api/history` | GET | Conversation messages for MCP |
|
|
222
|
+
| `/api/state` | GET/POST | Get or sync browser state |
|
|
223
|
+
| `/api/session` | GET/POST/DELETE | Claude Code session management |
|
|
224
|
+
| `/api/pending` | GET | Check for pending IPC messages |
|
|
225
|
+
| `/api/respond` | POST | Send IPC response |
|
|
226
|
+
| `/api/send` | POST | Send message with SSE streaming |
|
|
227
|
+
| `/api/claude-code` | POST | Execute Claude CLI, stream response |
|
|
228
|
+
| `/api/analyze-image` | POST | Claude vision analysis |
|
|
229
|
+
| `/api/open-url` | POST | Open URL in native browser |
|
|
230
|
+
| `/api/conversations` | GET/POST | List or create conversations |
|
|
231
|
+
| `/api/conversations/:id` | GET/PATCH/DELETE | Get, update, or delete conversation |
|
|
232
|
+
| `/api/conversations/:id/messages` | POST | Add message with images/activities |
|
|
233
|
+
| `/api/search` | GET | Full-text search across messages |
|
|
234
|
+
| `/api/migrate` | POST | Migrate localStorage to server |
|
|
235
|
+
|
|
236
|
+
See [docs/API.md](docs/API.md) for detailed request/response formats.
|
|
237
|
+
|
|
238
|
+
## Settings
|
|
239
|
+
|
|
240
|
+
Configurable via the settings drawer:
|
|
241
|
+
|
|
242
|
+
- **Claude Code mode**: Toggle between CLI and Direct API
|
|
243
|
+
- **Session connection**: View/disconnect Claude Code session
|
|
244
|
+
- **TTS**: Enable/disable text-to-speech
|
|
245
|
+
- **Continuous listening**: Auto-restart recording after responses
|
|
246
|
+
- **Trigger word**: Custom end-of-turn word (default: "over")
|
|
247
|
+
- **Silence delay**: Wait time after trigger word (0.5-3.0s)
|
|
248
|
+
- **API key**: Anthropic API key for Direct API mode
|
|
249
|
+
- **Reset onboarding**: Re-run the setup wizard
|
|
250
|
+
|
|
251
|
+
## Development
|
|
252
|
+
|
|
253
|
+
```bash
|
|
254
|
+
npm install # Install dependencies
|
|
255
|
+
npm run dev # Vite dev server (frontend only)
|
|
256
|
+
npm run build # Production build
|
|
257
|
+
npm run test # Run tests
|
|
258
|
+
npm run lint # ESLint
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
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.talkie.server'
|
|
12
|
+
const PLIST_PATH = join(homedir(), 'Library', 'LaunchAgents', `${PLIST_NAME}.plist`)
|
|
13
|
+
const LOG_DIR = join(homedir(), '.talkie', 'logs')
|
|
14
|
+
const PID_FILE = join(homedir(), '.talkie', 'server.pid')
|
|
15
|
+
const PORT = parseInt(process.env.TALKIE_PORT || '5173', 10)
|
|
16
|
+
|
|
17
|
+
function ensureDirs() {
|
|
18
|
+
const talkieDir = join(homedir(), '.talkie')
|
|
19
|
+
if (!existsSync(talkieDir)) {
|
|
20
|
+
mkdirSync(talkieDir, { 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 Talkie 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(`Talkie server running at https://localhost:${PORT}`)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.error('Server failed to start. Check logs with: talkie-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('Talkie 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: talkie-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
|
+
Talkie Server CLI
|
|
316
|
+
|
|
317
|
+
Usage: talkie-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
|
+
TALKIE_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/talkie.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.TALKIE_PORT || '5173', 10)
|
|
7
|
+
const URL = `https://localhost:${PORT}`
|
|
8
|
+
|
|
9
|
+
async function main() {
|
|
10
|
+
console.log('Starting Talkie...')
|
|
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('\nTalkie is running. Press Ctrl+C to stop.')
|
|
27
|
+
} catch (err) {
|
|
28
|
+
if (err instanceof Error && err.message.includes('already in use')) {
|
|
29
|
+
console.log(`Talkie 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 Talkie:', err)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
main()
|
|
Binary file
|