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 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