haltija 1.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.
@@ -0,0 +1,591 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Haltija CLI
4
+ *
5
+ * Usage:
6
+ * npx haltija # Launch desktop app (or server if electron unavailable)
7
+ * npx haltija --server # Server only (for CI, headless, bookmarklet usage)
8
+ * npx haltija --app # Explicitly launch desktop app
9
+ * npx haltija --https # Start HTTPS server (auto-generates certs)
10
+ * npx haltija --headless # Start with headless Chromium (for CI)
11
+ * npx haltija --setup-mcp # Configure Claude Desktop integration
12
+ * npx haltija --help # Show help
13
+ */
14
+
15
+ import { spawn, execSync as execSyncImported } from 'child_process'
16
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs'
17
+ import { homedir, platform } from 'os'
18
+ import { fileURLToPath } from 'url'
19
+ import { dirname, join } from 'path'
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url))
22
+ const serverPath = join(__dirname, '../dist/server.js')
23
+
24
+ const args = process.argv.slice(2)
25
+
26
+ // Colors for terminal output
27
+ const green = (s) => `\x1b[32m${s}\x1b[0m`
28
+ const yellow = (s) => `\x1b[33m${s}\x1b[0m`
29
+ const red = (s) => `\x1b[31m${s}\x1b[0m`
30
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`
31
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`
32
+
33
+ if (args.includes('--help') || args.includes('-h')) {
34
+ console.log(`
35
+ ${bold('haltija')} - Browser control for AI agents
36
+
37
+ Usage:
38
+ haltija [options]
39
+
40
+ Modes:
41
+ ${dim('(default)')} Launch desktop app if electron available, otherwise server
42
+ --app Explicitly launch desktop app (Electron)
43
+ --server Server only (for CI, headless, or bookmarklet usage)
44
+ --headless Start with headless Chromium browser (for CI)
45
+
46
+ Options:
47
+ --http HTTP only on port 8700 (default protocol)
48
+ --https HTTPS only on port 8701 (auto-generates certs)
49
+ --both Both HTTP (8700) and HTTPS (8701)
50
+ --headless-url <url> URL to open in headless browser (default: none)
51
+ --snapshots-dir <path> Save snapshots to disk (for CI artifacts)
52
+ --docs-dir <path> Directory with custom docs (*.md files)
53
+ --port <n> Set HTTP port (default: 8700)
54
+ --https-port <n> Set HTTPS port (default: 8701)
55
+ --setup-mcp Configure Claude Desktop MCP integration
56
+ --setup-mcp-check Check MCP configuration status
57
+ --setup-mcp-remove Remove Haltija from Claude Desktop config
58
+ --help, -h Show this help
59
+
60
+ Environment Variables:
61
+ DEV_CHANNEL_PORT HTTP port (default: 8700)
62
+ DEV_CHANNEL_HTTPS_PORT HTTPS port (default: 8701)
63
+ DEV_CHANNEL_MODE 'http', 'https', or 'both' (default: 'http')
64
+ DEV_CHANNEL_SNAPSHOTS_DIR Directory to save snapshots (default: memory only)
65
+ DEV_CHANNEL_DOCS_DIR Directory with custom docs (default: built-in only)
66
+
67
+ Subcommands:
68
+ haltija <command> [args] Run API commands directly (see hj --help)
69
+ haltija tree DOM tree with ref IDs
70
+ haltija click @42 Click element by ref
71
+ haltija type @10 Hello Type text into element
72
+ haltija eval document.title Run JS in browser
73
+ haltija status Server status
74
+
75
+ Use 'hj' as a short alias: hj tree, hj click @42, etc.
76
+
77
+ Examples:
78
+ haltija # Desktop app (or server fallback)
79
+ haltija --app # Desktop app explicitly
80
+ haltija --server # Server only
81
+ haltija --server --https # HTTPS server only
82
+ haltija --headless # Headless browser for CI
83
+ haltija --setup-mcp # Configure Claude Desktop integration
84
+ `)
85
+ process.exit(0)
86
+ }
87
+
88
+ // ============================================
89
+ // MCP Setup Functions
90
+ // ============================================
91
+
92
+ /** Get Claude Desktop config path based on platform */
93
+ function getClaudeDesktopConfigPath() {
94
+ const home = homedir()
95
+ switch (platform()) {
96
+ case 'darwin':
97
+ return join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
98
+ case 'win32':
99
+ return join(home, 'AppData', 'Roaming', 'Claude', 'claude_desktop_config.json')
100
+ case 'linux':
101
+ return join(home, '.config', 'claude', 'claude_desktop_config.json')
102
+ default:
103
+ return join(home, '.config', 'claude', 'claude_desktop_config.json')
104
+ }
105
+ }
106
+
107
+ /** Find the Haltija MCP server entry point */
108
+ function findMcpServerPath() {
109
+ const candidates = [
110
+ join(__dirname, '../apps/mcp/build/index.js'),
111
+ join(__dirname, 'mcp/build/index.js'),
112
+ join(process.cwd(), 'node_modules/haltija/apps/mcp/build/index.js'),
113
+ ]
114
+
115
+ for (const p of candidates) {
116
+ if (existsSync(p)) return p
117
+ }
118
+ return null
119
+ }
120
+
121
+ /** Check MCP configuration status */
122
+ function checkMcpConfig() {
123
+ console.log(bold('\nHaltija MCP Configuration Status\n'))
124
+
125
+ const configPath = getClaudeDesktopConfigPath()
126
+ const mcpPath = findMcpServerPath()
127
+
128
+ // Check Claude Desktop
129
+ if (existsSync(dirname(configPath))) {
130
+ console.log(green('✓') + ' Claude Desktop detected')
131
+ console.log(dim(` Config: ${configPath}`))
132
+
133
+ if (existsSync(configPath)) {
134
+ try {
135
+ const config = JSON.parse(readFileSync(configPath, 'utf8'))
136
+ if (config.mcpServers?.haltija) {
137
+ console.log(green('✓') + ' Haltija is configured in Claude Desktop')
138
+ console.log(dim(` Command: ${config.mcpServers.haltija.command} ${config.mcpServers.haltija.args?.join(' ') || ''}`))
139
+ } else {
140
+ console.log(yellow('○') + ' Haltija is not configured')
141
+ console.log(dim(` Run: haltija --setup-mcp`))
142
+ }
143
+ } catch {
144
+ console.log(yellow('○') + ' Config exists but could not be parsed')
145
+ }
146
+ } else {
147
+ console.log(yellow('○') + ' Config file does not exist yet')
148
+ console.log(dim(` Run: haltija --setup-mcp`))
149
+ }
150
+ } else {
151
+ console.log(yellow('○') + ' Claude Desktop not detected')
152
+ console.log(dim(` Install from: https://claude.ai/download`))
153
+ }
154
+
155
+ // Check MCP server
156
+ if (mcpPath) {
157
+ console.log(green('✓') + ' MCP server found')
158
+ console.log(dim(` Path: ${mcpPath}`))
159
+ } else {
160
+ console.log(red('✗') + ' MCP server not found')
161
+ console.log(dim(` Run from haltija directory or rebuild`))
162
+ }
163
+
164
+ // Check if server is running
165
+ fetch('http://localhost:8700/status', { signal: AbortSignal.timeout(1000) })
166
+ .then(r => {
167
+ if (r.ok) console.log(green('✓') + ' Haltija server is running on port 8700')
168
+ else console.log(yellow('○') + ' Haltija server not running')
169
+ })
170
+ .catch(() => {
171
+ console.log(yellow('○') + ' Haltija server not running')
172
+ console.log(dim(` Start with: haltija`))
173
+ })
174
+ .finally(() => {
175
+ console.log('')
176
+ })
177
+ }
178
+
179
+ /** Setup Claude Desktop MCP integration */
180
+ function setupMcp() {
181
+ const mcpPath = findMcpServerPath()
182
+ if (!mcpPath) {
183
+ console.log(red('Error:') + ' Could not find Haltija MCP server.')
184
+ console.log('Make sure you run this from the haltija directory or have it installed.')
185
+ process.exit(1)
186
+ }
187
+
188
+ const configPath = getClaudeDesktopConfigPath()
189
+ const configDir = dirname(configPath)
190
+
191
+ // Create config directory if needed
192
+ if (!existsSync(configDir)) {
193
+ console.log(dim(`Creating config directory: ${configDir}`))
194
+ mkdirSync(configDir, { recursive: true })
195
+ }
196
+
197
+ // Read or create config
198
+ let config = { mcpServers: {} }
199
+ if (existsSync(configPath)) {
200
+ try {
201
+ config = JSON.parse(readFileSync(configPath, 'utf8'))
202
+ if (!config.mcpServers) config.mcpServers = {}
203
+ } catch {
204
+ // Backup invalid config
205
+ const backupPath = configPath + '.backup'
206
+ console.log(yellow('Warning:') + ` Existing config invalid, backing up to ${backupPath}`)
207
+ writeFileSync(backupPath, readFileSync(configPath))
208
+ config = { mcpServers: {} }
209
+ }
210
+ }
211
+
212
+ // Check if already configured
213
+ if (config.mcpServers.haltija) {
214
+ console.log(green('✓') + ' Haltija is already configured in Claude Desktop')
215
+ console.log(dim(` Config: ${configPath}`))
216
+ console.log('')
217
+ console.log('To reconfigure, first run: ' + bold('haltija --setup-mcp-remove'))
218
+ process.exit(0)
219
+ }
220
+
221
+ // Add Haltija
222
+ config.mcpServers.haltija = {
223
+ command: 'node',
224
+ args: [mcpPath]
225
+ }
226
+
227
+ try {
228
+ writeFileSync(configPath, JSON.stringify(config, null, 2))
229
+ console.log(green('✓') + ' Haltija configured successfully!')
230
+ console.log(dim(` Config: ${configPath}`))
231
+ console.log('')
232
+ console.log(bold('Next steps:'))
233
+ console.log(' 1. ' + yellow('Restart Claude Desktop') + ' to load the MCP server')
234
+ console.log(' 2. Start Haltija server: ' + dim('haltija'))
235
+ console.log(' 3. Connect browser and chat with Claude!')
236
+ console.log('')
237
+ } catch (err) {
238
+ console.log(red('Error:') + ` Failed to write config: ${err.message}`)
239
+ process.exit(1)
240
+ }
241
+ }
242
+
243
+ /** Remove Haltija from Claude Desktop config */
244
+ function removeMcp() {
245
+ const configPath = getClaudeDesktopConfigPath()
246
+
247
+ if (!existsSync(configPath)) {
248
+ console.log('Claude Desktop config does not exist.')
249
+ process.exit(0)
250
+ }
251
+
252
+ try {
253
+ const config = JSON.parse(readFileSync(configPath, 'utf8'))
254
+ if (!config.mcpServers?.haltija) {
255
+ console.log('Haltija is not configured in Claude Desktop.')
256
+ process.exit(0)
257
+ }
258
+
259
+ delete config.mcpServers.haltija
260
+ writeFileSync(configPath, JSON.stringify(config, null, 2))
261
+
262
+ console.log(green('✓') + ' Removed Haltija from Claude Desktop config')
263
+ console.log(dim(` Config: ${configPath}`))
264
+ console.log('')
265
+ console.log(yellow('Restart Claude Desktop') + ' to apply changes.')
266
+ console.log('')
267
+ } catch (err) {
268
+ console.log(red('Error:') + ` Failed to update config: ${err.message}`)
269
+ process.exit(1)
270
+ }
271
+ }
272
+
273
+ // ============================================
274
+ // CLI Subcommand Detection
275
+ // ============================================
276
+
277
+ import { isSubcommand, runSubcommand } from './cli-subcommand.mjs'
278
+
279
+ // Check if first positional arg is a subcommand (not a flag, not a port number)
280
+ const firstNonFlag = args.find(a => !a.startsWith('-'))
281
+ if (firstNonFlag && isSubcommand(firstNonFlag)) {
282
+ // Parse --port for subcommand mode
283
+ let subPort = process.env.DEV_CHANNEL_PORT || '8700'
284
+ const subPortIdx = args.indexOf('--port')
285
+ if (subPortIdx !== -1 && args[subPortIdx + 1]) {
286
+ subPort = args[subPortIdx + 1]
287
+ }
288
+ // Collect args after the subcommand, removing --port <n>
289
+ const subIdx = args.indexOf(firstNonFlag)
290
+ const rawSubArgs = args.slice(subIdx + 1)
291
+ const cleanSubArgs = []
292
+ for (let i = 0; i < rawSubArgs.length; i++) {
293
+ if (rawSubArgs[i] === '--port') { i++; continue }
294
+ cleanSubArgs.push(rawSubArgs[i])
295
+ }
296
+ await runSubcommand(firstNonFlag, cleanSubArgs, subPort)
297
+ process.exit(0)
298
+ }
299
+
300
+ // Handle MCP setup commands
301
+ if (args.includes('--setup-mcp')) {
302
+ setupMcp()
303
+ process.exit(0)
304
+ }
305
+
306
+ if (args.includes('--setup-mcp-check')) {
307
+ checkMcpConfig()
308
+ // Don't exit immediately - let the async fetch complete
309
+ setTimeout(() => process.exit(0), 2000)
310
+ }
311
+
312
+ if (args.includes('--setup-mcp-remove')) {
313
+ removeMcp()
314
+ process.exit(0)
315
+ }
316
+
317
+ // Set up environment from args
318
+ const env = { ...process.env }
319
+
320
+ if (args.includes('--https')) {
321
+ env.DEV_CHANNEL_MODE = 'https'
322
+ } else if (args.includes('--both')) {
323
+ env.DEV_CHANNEL_MODE = 'both'
324
+ } else {
325
+ env.DEV_CHANNEL_MODE = env.DEV_CHANNEL_MODE || 'http'
326
+ }
327
+
328
+ const portIdx = args.indexOf('--port')
329
+ if (portIdx !== -1 && args[portIdx + 1]) {
330
+ env.DEV_CHANNEL_PORT = args[portIdx + 1]
331
+ }
332
+
333
+ const httpsPortIdx = args.indexOf('--https-port')
334
+ if (httpsPortIdx !== -1 && args[httpsPortIdx + 1]) {
335
+ env.DEV_CHANNEL_HTTPS_PORT = args[httpsPortIdx + 1]
336
+ }
337
+
338
+ // Legacy: first positional arg as port
339
+ const firstArg = args.find(a => !a.startsWith('-'))
340
+ if (firstArg && !isNaN(parseInt(firstArg))) {
341
+ env.DEV_CHANNEL_PORT = firstArg
342
+ }
343
+
344
+ // Snapshots directory for CI artifact upload
345
+ const snapshotsDirIdx = args.indexOf('--snapshots-dir')
346
+ if (snapshotsDirIdx !== -1 && args[snapshotsDirIdx + 1]) {
347
+ env.DEV_CHANNEL_SNAPSHOTS_DIR = args[snapshotsDirIdx + 1]
348
+ }
349
+
350
+ // Custom docs directory
351
+ const docsDirIdx = args.indexOf('--docs-dir')
352
+ if (docsDirIdx !== -1 && args[docsDirIdx + 1]) {
353
+ env.DEV_CHANNEL_DOCS_DIR = args[docsDirIdx + 1]
354
+ }
355
+
356
+ // ============================================
357
+ // Mode Detection
358
+ // ============================================
359
+
360
+ const headlessMode = args.includes('--headless')
361
+ const headlessUrlIdx = args.indexOf('--headless-url')
362
+ const headlessUrl = headlessUrlIdx !== -1 ? args[headlessUrlIdx + 1] : null
363
+ const explicitServer = args.includes('--server')
364
+ const explicitApp = args.includes('--app')
365
+
366
+ /** Detect if Electron desktop app is available */
367
+ function detectElectron() {
368
+ const desktopDir = join(__dirname, '../apps/desktop')
369
+ if (!existsSync(desktopDir)) return null
370
+
371
+ // Check for electron in desktop app's node_modules
372
+ const electronBin = join(desktopDir, 'node_modules/.bin/electron')
373
+ if (existsSync(electronBin)) return { electronBin, desktopDir }
374
+
375
+ // Check if electron is available globally
376
+ try {
377
+ execSyncImported('electron --version', { stdio: 'ignore', timeout: 5000 })
378
+ return { electronBin: 'electron', desktopDir }
379
+ } catch {}
380
+
381
+ return null
382
+ }
383
+
384
+ /** Read version from package.json */
385
+ function getVersion() {
386
+ try {
387
+ const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
388
+ return pkg.version || '0.0.0'
389
+ } catch {
390
+ return '0.0.0'
391
+ }
392
+ }
393
+
394
+ /** Print startup banner */
395
+ function printBanner(mode, port) {
396
+ const version = getVersion()
397
+ const url = `http://localhost:${port}`
398
+ console.log('')
399
+ console.log(dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
400
+ console.log(` ${bold('Haltija')} v${version} ${dim('—')} ${green(url)}`)
401
+ console.log(` Mode: ${mode === 'app' ? 'Desktop App' : mode === 'headless' ? 'Headless' : 'Server'}`)
402
+ console.log('')
403
+ console.log(dim(' Agent setup:'))
404
+ console.log(` MCP: ${dim('bunx haltija --setup-mcp')}`)
405
+ console.log(` Curl: ${dim(`curl ${url}/tree`)}`)
406
+ console.log(` Docs: ${dim(`curl ${url}/docs`)}`)
407
+ console.log(dim('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
408
+ console.log('')
409
+ }
410
+
411
+ /** Launch the Electron desktop app */
412
+ function launchApp(electronInfo, port) {
413
+ printBanner('app', port)
414
+
415
+ const child = spawn(electronInfo.electronBin, [electronInfo.desktopDir], {
416
+ env: { ...env, DEV_CHANNEL_PORT: String(port) },
417
+ stdio: 'inherit'
418
+ })
419
+
420
+ child.on('error', (err) => {
421
+ console.error(red('Error:') + ` Failed to launch desktop app: ${err.message}`)
422
+ console.log(dim('Falling back to server mode...'))
423
+ console.log('')
424
+ startServer(port)
425
+ })
426
+
427
+ child.on('exit', code => {
428
+ process.exit(code || 0)
429
+ })
430
+ }
431
+
432
+ /** Start the server (current behavior) */
433
+ function startServer(port) {
434
+ printBanner('server', port)
435
+ tryBun()
436
+ }
437
+
438
+ // Start headless browser after server is ready
439
+ const startHeadlessBrowser = async (port) => {
440
+ console.log('[tosijs-dev] Starting headless Chromium browser...')
441
+
442
+ try {
443
+ // Dynamic import to avoid requiring playwright if not using headless mode
444
+ const { chromium } = await import('playwright')
445
+
446
+ const browser = await chromium.launch({
447
+ headless: true,
448
+ })
449
+
450
+ const context = await browser.newContext({
451
+ viewport: { width: 1280, height: 720 },
452
+ })
453
+
454
+ const page = await context.newPage()
455
+
456
+ // Inject tosijs-dev widget into every page
457
+ await context.addInitScript({
458
+ content: `
459
+ // Auto-inject tosijs-dev widget
460
+ (function() {
461
+ if (document.querySelector('tosijs-dev')) return;
462
+
463
+ console.log(
464
+ '%c🦉 tosijs-dev%c headless mode',
465
+ 'background:#6366f1;color:white;padding:2px 8px;border-radius:3px 0 0 3px;font-weight:bold',
466
+ 'background:#22c55e;color:white;padding:2px 8px;border-radius:0 3px 3px 0'
467
+ );
468
+
469
+ fetch('http://localhost:${port}/inject.js')
470
+ .then(r => r.text())
471
+ .then(eval)
472
+ .catch(e => console.error('[tosijs-dev] Failed to inject:', e));
473
+ })();
474
+ `
475
+ })
476
+
477
+ // Navigate to URL if specified
478
+ if (headlessUrl) {
479
+ console.log(`[tosijs-dev] Opening ${headlessUrl}...`)
480
+ await page.goto(headlessUrl, { waitUntil: 'domcontentloaded' })
481
+ } else {
482
+ // Navigate to test page by default
483
+ await page.goto(`http://localhost:${port}/`, { waitUntil: 'domcontentloaded' })
484
+ }
485
+
486
+ console.log('[tosijs-dev] Headless browser ready. Widget auto-injected.')
487
+ console.log('[tosijs-dev] Use POST /navigate to change pages.')
488
+
489
+ // Keep browser alive
490
+ process.on('SIGINT', async () => {
491
+ console.log('\\n[tosijs-dev] Closing browser...')
492
+ await browser.close()
493
+ process.exit(0)
494
+ })
495
+
496
+ process.on('SIGTERM', async () => {
497
+ await browser.close()
498
+ process.exit(0)
499
+ })
500
+
501
+ } catch (err) {
502
+ if (err.code === 'ERR_MODULE_NOT_FOUND') {
503
+ console.error('[tosijs-dev] Playwright not installed. Run: npm install playwright')
504
+ console.error('[tosijs-dev] Then: npx playwright install chromium')
505
+ } else {
506
+ console.error('[tosijs-dev] Failed to start headless browser:', err.message)
507
+ }
508
+ process.exit(1)
509
+ }
510
+ }
511
+
512
+ // Try bun first, fall back to node
513
+ const tryBun = () => {
514
+ const bun = spawn('bun', ['run', serverPath], {
515
+ env,
516
+ stdio: 'inherit'
517
+ })
518
+
519
+ bun.on('error', () => {
520
+ // Bun not available, try node
521
+ tryNode()
522
+ })
523
+
524
+ bun.on('exit', code => {
525
+ process.exit(code || 0)
526
+ })
527
+
528
+ // Start headless browser after a short delay for server to start
529
+ if (headlessMode) {
530
+ setTimeout(() => {
531
+ startHeadlessBrowser(env.DEV_CHANNEL_PORT || 8700)
532
+ }, 2000)
533
+ }
534
+ }
535
+
536
+ const tryNode = () => {
537
+ console.log('[tosijs-dev] Bun not found, using Node.js (some features may be limited)')
538
+
539
+ const node = spawn('node', [serverPath], {
540
+ env,
541
+ stdio: 'inherit'
542
+ })
543
+
544
+ node.on('error', (err) => {
545
+ console.error('Failed to start server:', err.message)
546
+ process.exit(1)
547
+ })
548
+
549
+ node.on('exit', code => {
550
+ process.exit(code || 0)
551
+ })
552
+
553
+ // Start headless browser after a short delay for server to start
554
+ if (headlessMode) {
555
+ setTimeout(() => {
556
+ startHeadlessBrowser(env.DEV_CHANNEL_PORT || 8700)
557
+ }, 2000)
558
+ }
559
+ }
560
+
561
+ // ============================================
562
+ // Launch Mode Selection
563
+ // ============================================
564
+
565
+ const port = env.DEV_CHANNEL_PORT || '8700'
566
+
567
+ if (headlessMode) {
568
+ // Headless mode: start server + headless browser
569
+ printBanner('headless', port)
570
+ tryBun()
571
+ } else if (explicitServer) {
572
+ // Explicit server-only mode
573
+ startServer(port)
574
+ } else if (explicitApp) {
575
+ // Explicit app mode - fail if electron not available
576
+ const electronInfo = detectElectron()
577
+ if (!electronInfo) {
578
+ console.error(red('Error:') + ' Desktop app not available.')
579
+ console.log(dim('Electron not found. Install it in apps/desktop/ or use --server mode.'))
580
+ process.exit(1)
581
+ }
582
+ launchApp(electronInfo, port)
583
+ } else {
584
+ // Default: try app, fall back to server
585
+ const electronInfo = detectElectron()
586
+ if (electronInfo) {
587
+ launchApp(electronInfo, port)
588
+ } else {
589
+ startServer(port)
590
+ }
591
+ }
@@ -0,0 +1,74 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * tosijs-dev CLI (Bun version)
4
+ *
5
+ * Usage:
6
+ * bunx tosijs-dev # Start HTTP server on port 8700
7
+ * bunx tosijs-dev --https # Start HTTPS server (auto-generates certs)
8
+ * bunx tosijs-dev --both # Start both HTTP and HTTPS
9
+ * bunx tosijs-dev --help # Show help
10
+ */
11
+
12
+ const args = process.argv.slice(2)
13
+
14
+ if (args.includes('--help') || args.includes('-h')) {
15
+ console.log(`
16
+ tosijs-dev - Browser control for AI agents
17
+
18
+ Usage:
19
+ tosijs-dev [options]
20
+
21
+ Options:
22
+ --http HTTP only on port 8700 (default)
23
+ --https HTTPS only on port 8701 (auto-generates certs)
24
+ --both Both HTTP (8700) and HTTPS (8701)
25
+ --port <n> Set HTTP port (default: 8700)
26
+ --https-port <n> Set HTTPS port (default: 8701)
27
+ --help, -h Show this help
28
+
29
+ Environment Variables:
30
+ DEV_CHANNEL_PORT HTTP port (default: 8700)
31
+ DEV_CHANNEL_HTTPS_PORT HTTPS port (default: 8701)
32
+ DEV_CHANNEL_MODE 'http', 'https', or 'both' (default: 'http')
33
+
34
+ Examples:
35
+ tosijs-dev # HTTP on 8700
36
+ tosijs-dev --https # HTTPS on 8701 (generates certs with mkcert or openssl)
37
+ tosijs-dev --both # HTTP on 8700 + HTTPS on 8701
38
+ tosijs-dev --port 3000 # HTTP on 3000
39
+
40
+ Once running, curl the /docs endpoint for full API documentation.
41
+ `)
42
+ process.exit(0)
43
+ }
44
+
45
+ // Parse args
46
+ if (args.includes('--https')) {
47
+ process.env.DEV_CHANNEL_MODE = 'https'
48
+ } else if (args.includes('--both')) {
49
+ process.env.DEV_CHANNEL_MODE = 'both'
50
+ } else {
51
+ process.env.DEV_CHANNEL_MODE = process.env.DEV_CHANNEL_MODE || 'http'
52
+ }
53
+
54
+ const portIdx = args.indexOf('--port')
55
+ if (portIdx !== -1 && args[portIdx + 1]) {
56
+ process.env.DEV_CHANNEL_PORT = args[portIdx + 1]
57
+ }
58
+
59
+ const httpsPortIdx = args.indexOf('--https-port')
60
+ if (httpsPortIdx !== -1 && args[httpsPortIdx + 1]) {
61
+ process.env.DEV_CHANNEL_HTTPS_PORT = args[httpsPortIdx + 1]
62
+ }
63
+
64
+ // Legacy: first positional arg as port
65
+ const firstArg = args.find(a => !a.startsWith('-'))
66
+ if (firstArg && !isNaN(parseInt(firstArg))) {
67
+ process.env.DEV_CHANNEL_PORT = firstArg
68
+ }
69
+
70
+ // Import and start server (the server prints its own startup message)
71
+ import('../dist/server.js').catch(err => {
72
+ console.error('Failed to start server:', err)
73
+ process.exit(1)
74
+ })