@miphamai/cli 0.31.1 → 0.32.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/mipham.ts CHANGED
@@ -312,6 +312,755 @@ async function runUpdate(): Promise<boolean> {
312
312
  process.exit(0)
313
313
  }
314
314
 
315
+ // ── Daemon lifecycle commands ──────────────────────────────────────────────────
316
+
317
+ async function runDaemonCLI(): Promise<boolean> {
318
+ const args = process.argv.slice(2)
319
+ if (args[0] !== 'daemon') return false
320
+
321
+ const subcmd = args[1]
322
+
323
+ const { spawn } = await import('node:child_process')
324
+
325
+ if (subcmd === 'start') {
326
+ const { getDaemonStatus } = await import('../src/daemon/index')
327
+ const status = getDaemonStatus()
328
+ if (status) {
329
+ console.log(`Daemon already running (PID: ${status.pid}, Port: ${status.port})`)
330
+ process.exit(0)
331
+ }
332
+
333
+ console.log('Starting daemon...')
334
+ const daemonScript = new URL('./daemon.ts', import.meta.url).pathname
335
+ const child = spawn('bun', ['run', daemonScript], {
336
+ detached: true,
337
+ stdio: 'ignore',
338
+ env: { ...process.env },
339
+ })
340
+ child.unref()
341
+
342
+ // Wait briefly for daemon to write PID/port files
343
+ await new Promise((resolve) => setTimeout(resolve, 1000))
344
+
345
+ const newStatus = getDaemonStatus()
346
+ if (newStatus) {
347
+ console.log(`Daemon started (PID: ${newStatus.pid}, Port: ${newStatus.port})`)
348
+ } else {
349
+ console.log('Daemon started (PID unknown — check `mipham daemon status`)')
350
+ }
351
+ process.exit(0)
352
+ }
353
+
354
+ if (subcmd === 'stop') {
355
+ const { getDaemonStatus } = await import('../src/daemon/index')
356
+ const status = getDaemonStatus()
357
+ if (!status) {
358
+ console.log('Daemon is not running.')
359
+ process.exit(0)
360
+ }
361
+ try {
362
+ process.kill(status.pid, 'SIGTERM')
363
+ console.log(`Daemon stopped (PID: ${status.pid})`)
364
+ } catch {
365
+ console.log('Daemon is not running.')
366
+ }
367
+ process.exit(0)
368
+ }
369
+
370
+ if (subcmd === 'status') {
371
+ const { getDaemonStatus } = await import('../src/daemon/index')
372
+ const status = getDaemonStatus()
373
+ if (status) {
374
+ console.log(`Daemon: running`)
375
+ console.log(` PID: ${status.pid}`)
376
+ console.log(` Port: ${status.port}`)
377
+ console.log(` URL: http://127.0.0.1:${status.port}`)
378
+ } else {
379
+ console.log(`Daemon: not running`)
380
+ }
381
+ process.exit(0)
382
+ }
383
+
384
+ if (subcmd === 'restart') {
385
+ const { getDaemonStatus } = await import('../src/daemon/index')
386
+ const status = getDaemonStatus()
387
+ if (status) {
388
+ try {
389
+ process.kill(status.pid, 'SIGTERM')
390
+ } catch {
391
+ // Process may have already exited
392
+ }
393
+ await new Promise((resolve) => setTimeout(resolve, 500))
394
+ }
395
+
396
+ const daemonScript = new URL('./daemon.ts', import.meta.url).pathname
397
+ const child = spawn('bun', ['run', daemonScript], {
398
+ detached: true,
399
+ stdio: 'ignore',
400
+ env: { ...process.env },
401
+ })
402
+ child.unref()
403
+ await new Promise((resolve) => setTimeout(resolve, 1000))
404
+ console.log('Daemon restarted.')
405
+ process.exit(0)
406
+ }
407
+
408
+ // Unknown subcommand
409
+ console.error(`Unknown daemon command: mipham daemon ${subcmd}`)
410
+ console.error('Usage: mipham daemon start|stop|status|restart')
411
+ process.exit(1)
412
+ }
413
+
414
+ // ── Attach: connect TUI to a daemon session ────────────────────────────────
415
+
416
+ async function runAttachCLI(): Promise<boolean> {
417
+ const args = process.argv.slice(2)
418
+ if (args[0] !== 'attach') return false
419
+
420
+ const { getPort, getDaemonStatus } = await import('../src/daemon/index')
421
+ const { join } = await import('node:path')
422
+ const { readFileSync, existsSync } = await import('node:fs')
423
+ const { homedir } = await import('node:os')
424
+
425
+ // Check if daemon is running
426
+ const status = getDaemonStatus()
427
+ if (!status) {
428
+ console.error('No daemon running. Start one with: mipham daemon start')
429
+ process.exit(1)
430
+ }
431
+
432
+ const port = getPort()
433
+ const tokenPath = join(homedir(), '.mipham', 'daemon.token')
434
+ let token = ''
435
+ if (existsSync(tokenPath)) {
436
+ token = readFileSync(tokenPath, 'utf-8').trim()
437
+ }
438
+
439
+ // Fetch active sessions from daemon
440
+ interface SessionInfo {
441
+ id: string
442
+ name: string
443
+ cwd: string
444
+ status: string
445
+ model: string
446
+ updatedAt: string
447
+ }
448
+
449
+ let sessions: SessionInfo[] = []
450
+ try {
451
+ const resp = await fetch(`http://127.0.0.1:${port}/api/v1/sessions`)
452
+ const data = (await resp.json()) as { ok: boolean; data?: { sessions: SessionInfo[] } }
453
+ if (data.ok && data.data) {
454
+ sessions = data.data.sessions.filter((s) => s.status === 'active' || s.status === 'idle')
455
+ }
456
+ } catch (err) {
457
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
458
+ process.exit(1)
459
+ }
460
+
461
+ const latestFlag = args.includes('--latest')
462
+ const sessionIdArg = args[1] && !args[1].startsWith('-') ? args[1] : undefined
463
+
464
+ let targetSession: SessionInfo | null = null
465
+
466
+ if (latestFlag) {
467
+ if (sessions.length === 0) {
468
+ console.error('No active sessions found.')
469
+ process.exit(1)
470
+ }
471
+ targetSession = sessions[0]! // Most recently updated active/idle session
472
+ } else if (sessionIdArg) {
473
+ targetSession =
474
+ sessions.find((s) => s.id === sessionIdArg || s.id.startsWith(sessionIdArg)) ?? null
475
+ if (!targetSession) {
476
+ console.error(`Session "${sessionIdArg}" not found.`)
477
+ if (sessions.length > 0) {
478
+ console.error(`Available sessions:`)
479
+ for (const s of sessions) {
480
+ console.error(` ${s.id.slice(0, 8)} ${s.name} [${s.status}] ${s.cwd}`)
481
+ }
482
+ }
483
+ process.exit(1)
484
+ }
485
+ } else {
486
+ // List sessions and show usage
487
+ if (sessions.length === 0) {
488
+ console.log('No active sessions.')
489
+ } else {
490
+ console.log('Active sessions:')
491
+ for (const s of sessions) {
492
+ const shortId = s.id.slice(0, 8)
493
+ console.log(` ${shortId} ${s.name} [${s.status}] ${s.model} ${s.cwd}`)
494
+ }
495
+ }
496
+ console.log()
497
+ console.log('Usage:')
498
+ console.log(' mipham attach <id> Attach to a specific session')
499
+ console.log(' mipham attach --latest Attach to the most recent session')
500
+ process.exit(0)
501
+ }
502
+
503
+ // Read version for the TUI header
504
+ let version = '0.0.0'
505
+ try {
506
+ const pkgPath = join(import.meta.dirname!, '..', 'package.json')
507
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
508
+ version = pkg.version
509
+ } catch {
510
+ // fallback
511
+ }
512
+
513
+ // Launch TUI in remote mode — the engine will connect via WebSocket
514
+ const { runApp } = await import('../src/index')
515
+ await runApp({
516
+ version,
517
+ remoteSession: {
518
+ sessionId: targetSession.id,
519
+ port,
520
+ token,
521
+ },
522
+ })
523
+ return true
524
+ }
525
+
526
+ // ── Agent CLI: inspect and manage daemon agents ─────────────────────────────
527
+
528
+ async function runAgentCLI(): Promise<boolean> {
529
+ const args = process.argv.slice(2)
530
+ if (args[0] !== 'agents' && args[0] !== 'agent') return false
531
+
532
+ const { getPort, getDaemonStatus } = await import('../src/daemon/index')
533
+ const { join } = await import('node:path')
534
+ const { readFileSync, existsSync } = await import('node:fs')
535
+ const { homedir } = await import('node:os')
536
+
537
+ // Check if daemon is running
538
+ const status = getDaemonStatus()
539
+ if (!status) {
540
+ console.error('No daemon running. Start one with: mipham daemon start')
541
+ process.exit(1)
542
+ }
543
+
544
+ const port = getPort()
545
+ const tokenPath = join(homedir(), '.mipham', 'daemon.token')
546
+ let token = ''
547
+ if (existsSync(tokenPath)) {
548
+ token = readFileSync(tokenPath, 'utf-8').trim()
549
+ }
550
+
551
+ const baseUrl = `http://127.0.0.1:${port}`
552
+
553
+ // ── mipham agents ──────────────────────────────────────────
554
+ if (args[0] === 'agents') {
555
+ try {
556
+ const resp = await fetch(`${baseUrl}/api/v1/agents`)
557
+ const data = (await resp.json()) as {
558
+ ok: boolean
559
+ data?: {
560
+ agents: Array<{
561
+ id: string
562
+ status: string
563
+ agentType: string
564
+ description: string
565
+ createdAt: string
566
+ sessionId: string
567
+ }>
568
+ }
569
+ }
570
+ if (!data.ok || !data.data) {
571
+ console.error('Failed to fetch agents.')
572
+ process.exit(1)
573
+ }
574
+ const agents = data.data.agents
575
+ if (agents.length === 0) {
576
+ console.log('No agents found.')
577
+ } else {
578
+ for (const a of agents) {
579
+ const shortId = a.id.slice(0, 12)
580
+ console.log(`${shortId} [${a.status}] ${a.agentType} ${a.description}`)
581
+ }
582
+ }
583
+ } catch (err) {
584
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
585
+ process.exit(1)
586
+ }
587
+ process.exit(0)
588
+ }
589
+
590
+ // ── mipham agent <id|subcommand> ───────────────────────────
591
+ const target = args[1]
592
+ if (!target) {
593
+ console.error('Usage: mipham agent <id|message|stop> [...]')
594
+ process.exit(1)
595
+ }
596
+
597
+ // mipham agent message <id> [--content <text>]
598
+ if (target === 'message') {
599
+ const agentId = args[2]
600
+ if (!agentId) {
601
+ console.error('Usage: mipham agent message <id> [--content <text>]')
602
+ process.exit(1)
603
+ }
604
+
605
+ let content = ''
606
+ const contentFlagIdx = args.indexOf('--content')
607
+ if (contentFlagIdx !== -1 && args[contentFlagIdx + 1]) {
608
+ content = args[contentFlagIdx + 1]!
609
+ } else {
610
+ // Prompt for content interactively
611
+ const { createInterface } = await import('node:readline')
612
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
613
+ content = await new Promise<string>((resolve) => {
614
+ rl.question('Message: ', (answer) => {
615
+ rl.close()
616
+ resolve(answer)
617
+ })
618
+ })
619
+ }
620
+
621
+ if (!content.trim()) {
622
+ console.error('Message content is required.')
623
+ process.exit(1)
624
+ }
625
+
626
+ try {
627
+ const resp = await fetch(`${baseUrl}/api/v1/agents/${agentId}/message`, {
628
+ method: 'POST',
629
+ headers: { 'Content-Type': 'application/json' },
630
+ body: JSON.stringify({ content }),
631
+ })
632
+ const data = (await resp.json()) as { ok: boolean; error?: string }
633
+ if (!data.ok) {
634
+ console.error(`Error: ${data.error || 'Unknown error'}`)
635
+ process.exit(1)
636
+ }
637
+ console.log(`Message sent to agent ${agentId}`)
638
+ } catch (err) {
639
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
640
+ process.exit(1)
641
+ }
642
+ process.exit(0)
643
+ }
644
+
645
+ // mipham agent stop <id>
646
+ if (target === 'stop') {
647
+ const agentId = args[2]
648
+ if (!agentId) {
649
+ console.error('Usage: mipham agent stop <id>')
650
+ process.exit(1)
651
+ }
652
+
653
+ try {
654
+ const resp = await fetch(`${baseUrl}/api/v1/agents/${agentId}`, { method: 'DELETE' })
655
+ const data = (await resp.json()) as { ok: boolean; error?: string }
656
+ if (!data.ok) {
657
+ console.error(`Error: ${data.error || 'Unknown error'}`)
658
+ process.exit(1)
659
+ }
660
+ console.log(`Agent ${agentId} stopped.`)
661
+ } catch (err) {
662
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
663
+ process.exit(1)
664
+ }
665
+ process.exit(0)
666
+ }
667
+
668
+ // mipham agent <id> — show detail
669
+ const agentId = target
670
+ try {
671
+ const resp = await fetch(`${baseUrl}/api/v1/agents/${agentId}`)
672
+ const data = (await resp.json()) as {
673
+ ok: boolean
674
+ data?: {
675
+ agent: {
676
+ id: string
677
+ agentType: string
678
+ description: string
679
+ status: string
680
+ kind: string
681
+ sessionId: string
682
+ createdAt: string
683
+ completedAt?: string | null
684
+ result?: string | null
685
+ error?: string | null
686
+ worktree?: string | null
687
+ branch?: string | null
688
+ prUrl?: string | null
689
+ parentId?: string | null
690
+ }
691
+ }
692
+ error?: string
693
+ }
694
+ if (!data.ok || !data.data) {
695
+ console.error(`Error: ${data.error || 'Agent not found'}`)
696
+ process.exit(1)
697
+ }
698
+ const a = data.data.agent
699
+ console.log(`Agent: ${a.id}`)
700
+ console.log(` Type: ${a.agentType}`)
701
+ console.log(` Description: ${a.description}`)
702
+ console.log(` Status: ${a.status}`)
703
+ console.log(` Kind: ${a.kind}`)
704
+ console.log(` Session: ${a.sessionId}`)
705
+ if (a.parentId) console.log(` Parent: ${a.parentId}`)
706
+ console.log(` Created: ${a.createdAt}`)
707
+ if (a.completedAt) console.log(` Completed: ${a.completedAt}`)
708
+ if (a.result) console.log(` Result: ${a.result}`)
709
+ if (a.error) console.log(` Error: ${a.error}`)
710
+ if (a.worktree) console.log(` Worktree: ${a.worktree}`)
711
+ if (a.branch) console.log(` Branch: ${a.branch}`)
712
+ if (a.prUrl) console.log(` PR: ${a.prUrl}`)
713
+ } catch (err) {
714
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
715
+ process.exit(1)
716
+ }
717
+ process.exit(0)
718
+ }
719
+
720
+ // ── Goal CLI: manage daemon goals ─────────────────────────────────────────────
721
+
722
+ async function runGoalCLI(): Promise<boolean> {
723
+ const args = process.argv.slice(2)
724
+ if (args[0] !== 'goal') return false
725
+
726
+ const { getPort, getDaemonStatus } = await import('../src/daemon/index')
727
+
728
+ // Check if daemon is running
729
+ const status = getDaemonStatus()
730
+ if (!status) {
731
+ console.error('No daemon running. Start one with: mipham daemon start')
732
+ process.exit(1)
733
+ }
734
+
735
+ const port = getPort()
736
+ const baseUrl = `http://127.0.0.1:${port}`
737
+
738
+ const subcmd = args[1]
739
+ if (!subcmd) {
740
+ console.error('Usage: mipham goal list|add|done|pause [options]')
741
+ process.exit(1)
742
+ }
743
+
744
+ // ── mipham goal list [--session <id>] ─────────────────────────
745
+ if (subcmd === 'list') {
746
+ const sessionIdx = args.indexOf('--session')
747
+ const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : undefined
748
+ if (!sessionId) {
749
+ console.error('Usage: mipham goal list --session <id>')
750
+ process.exit(1)
751
+ }
752
+
753
+ try {
754
+ const resp = await fetch(`${baseUrl}/api/v1/goals?session=${sessionId}`)
755
+ const data = (await resp.json()) as {
756
+ ok: boolean
757
+ data?: {
758
+ goals: Array<{
759
+ id: number
760
+ description: string
761
+ status: string
762
+ progress: { current: number; total: number } | null
763
+ createdAt: string
764
+ }>
765
+ }
766
+ }
767
+ if (!data.ok || !data.data) {
768
+ console.error('Failed to fetch goals.')
769
+ process.exit(1)
770
+ }
771
+ const goals = data.data.goals
772
+ if (goals.length === 0) {
773
+ console.log('No goals found.')
774
+ } else {
775
+ for (const g of goals) {
776
+ const progressStr = g.progress ? ` [${g.progress.current}/${g.progress.total}]` : ''
777
+ console.log(`#${g.id} [${g.status}]${progressStr} ${g.description}`)
778
+ }
779
+ }
780
+ } catch (err) {
781
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
782
+ process.exit(1)
783
+ }
784
+ process.exit(0)
785
+ }
786
+
787
+ // ── mipham goal add --session <id> --desc <text> [--current <n> --total <n>] ─
788
+ if (subcmd === 'add') {
789
+ const sessionIdx = args.indexOf('--session')
790
+ const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : undefined
791
+ const descIdx = args.indexOf('--desc')
792
+ const description = descIdx !== -1 ? args[descIdx + 1] : undefined
793
+
794
+ if (!sessionId || !description) {
795
+ console.error(
796
+ 'Usage: mipham goal add --session <id> --desc <text> [--current <n> --total <n>]',
797
+ )
798
+ process.exit(1)
799
+ }
800
+
801
+ const currentIdx = args.indexOf('--current')
802
+ const totalIdx = args.indexOf('--total')
803
+ const current = currentIdx !== -1 ? parseInt(args[currentIdx + 1]!, 10) : undefined
804
+ const total = totalIdx !== -1 ? parseInt(args[totalIdx + 1]!, 10) : undefined
805
+ const progress = current !== undefined && total !== undefined ? { current, total } : undefined
806
+
807
+ try {
808
+ const resp = await fetch(`${baseUrl}/api/v1/goals`, {
809
+ method: 'POST',
810
+ headers: { 'Content-Type': 'application/json' },
811
+ body: JSON.stringify({ sessionId, description, progress }),
812
+ })
813
+ const data = (await resp.json()) as { ok: boolean; data?: { id: number }; error?: string }
814
+ if (!data.ok) {
815
+ console.error(`Error: ${data.error || 'Unknown error'}`)
816
+ process.exit(1)
817
+ }
818
+ console.log(`Goal #${data.data!.id} created.`)
819
+ } catch (err) {
820
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
821
+ process.exit(1)
822
+ }
823
+ process.exit(0)
824
+ }
825
+
826
+ // ── mipham goal done <id> ──────────────────────────────────────
827
+ if (subcmd === 'done') {
828
+ const goalId = args[2]
829
+ if (!goalId) {
830
+ console.error('Usage: mipham goal done <id>')
831
+ process.exit(1)
832
+ }
833
+
834
+ try {
835
+ const resp = await fetch(`${baseUrl}/api/v1/goals/${goalId}`, {
836
+ method: 'PATCH',
837
+ headers: { 'Content-Type': 'application/json' },
838
+ body: JSON.stringify({ status: 'completed' }),
839
+ })
840
+ const data = (await resp.json()) as { ok: boolean; error?: string }
841
+ if (!data.ok) {
842
+ console.error(`Error: ${data.error || 'Unknown error'}`)
843
+ process.exit(1)
844
+ }
845
+ console.log(`Goal #${goalId} marked as done.`)
846
+ } catch (err) {
847
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
848
+ process.exit(1)
849
+ }
850
+ process.exit(0)
851
+ }
852
+
853
+ // ── mipham goal pause <id> ─────────────────────────────────────
854
+ if (subcmd === 'pause') {
855
+ const goalId = args[2]
856
+ if (!goalId) {
857
+ console.error('Usage: mipham goal pause <id>')
858
+ process.exit(1)
859
+ }
860
+
861
+ try {
862
+ const resp = await fetch(`${baseUrl}/api/v1/goals/${goalId}`, {
863
+ method: 'PATCH',
864
+ headers: { 'Content-Type': 'application/json' },
865
+ body: JSON.stringify({ status: 'paused' }),
866
+ })
867
+ const data = (await resp.json()) as { ok: boolean; error?: string }
868
+ if (!data.ok) {
869
+ console.error(`Error: ${data.error || 'Unknown error'}`)
870
+ process.exit(1)
871
+ }
872
+ console.log(`Goal #${goalId} paused.`)
873
+ } catch (err) {
874
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
875
+ process.exit(1)
876
+ }
877
+ process.exit(0)
878
+ }
879
+
880
+ console.error(`Unknown goal command: mipham goal ${subcmd}`)
881
+ console.error('Usage: mipham goal list|add|done|pause [options]')
882
+ process.exit(1)
883
+ }
884
+
885
+ // ── Schedule CLI: manage daemon schedules ─────────────────────────────────────
886
+
887
+ async function runScheduleCLI(): Promise<boolean> {
888
+ const args = process.argv.slice(2)
889
+ if (args[0] !== 'schedule') return false
890
+
891
+ const { getPort, getDaemonStatus } = await import('../src/daemon/index')
892
+
893
+ // Check if daemon is running
894
+ const status = getDaemonStatus()
895
+ if (!status) {
896
+ console.error('No daemon running. Start one with: mipham daemon start')
897
+ process.exit(1)
898
+ }
899
+
900
+ const port = getPort()
901
+ const baseUrl = `http://127.0.0.1:${port}`
902
+
903
+ const subcmd = args[1]
904
+ if (!subcmd) {
905
+ console.error('Usage: mipham schedule list|add|remove [options]')
906
+ process.exit(1)
907
+ }
908
+
909
+ // ── mipham schedule list [--session <id>] ──────────────────────
910
+ if (subcmd === 'list') {
911
+ const sessionIdx = args.indexOf('--session')
912
+ const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : undefined
913
+ if (!sessionId) {
914
+ console.error('Usage: mipham schedule list --session <id>')
915
+ process.exit(1)
916
+ }
917
+
918
+ try {
919
+ const resp = await fetch(`${baseUrl}/api/v1/schedules?session=${sessionId}`)
920
+ const data = (await resp.json()) as {
921
+ ok: boolean
922
+ data?: {
923
+ schedules: Array<{
924
+ id: number
925
+ cronExpr: string
926
+ prompt: string
927
+ enabled: boolean
928
+ lastFired: string | null
929
+ nextFire: string
930
+ }>
931
+ }
932
+ }
933
+ if (!data.ok || !data.data) {
934
+ console.error('Failed to fetch schedules.')
935
+ process.exit(1)
936
+ }
937
+ const schedules = data.data.schedules
938
+ if (schedules.length === 0) {
939
+ console.log('No schedules found.')
940
+ } else {
941
+ for (const s of schedules) {
942
+ const enabledStr = s.enabled ? 'enabled' : 'disabled'
943
+ const lastStr = s.lastFired ? ` last: ${s.lastFired}` : ''
944
+ console.log(`#${s.id} [${enabledStr}] ${s.cronExpr} next: ${s.nextFire}${lastStr}`)
945
+ console.log(` prompt: ${s.prompt}`)
946
+ }
947
+ }
948
+ } catch (err) {
949
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
950
+ process.exit(1)
951
+ }
952
+ process.exit(0)
953
+ }
954
+
955
+ // ── mipham schedule add --session <id> --cron <expr> --prompt <text> ──
956
+ if (subcmd === 'add') {
957
+ const sessionIdx = args.indexOf('--session')
958
+ const sessionId = sessionIdx !== -1 ? args[sessionIdx + 1] : undefined
959
+ const cronIdx = args.indexOf('--cron')
960
+ const cronExpr = cronIdx !== -1 ? args[cronIdx + 1] : undefined
961
+ const promptIdx = args.indexOf('--prompt')
962
+ const prompt = promptIdx !== -1 ? args[promptIdx + 1] : undefined
963
+
964
+ if (!sessionId || !cronExpr || !prompt) {
965
+ console.error('Usage: mipham schedule add --session <id> --cron <expr> --prompt <text>')
966
+ process.exit(1)
967
+ }
968
+
969
+ try {
970
+ const resp = await fetch(`${baseUrl}/api/v1/schedules`, {
971
+ method: 'POST',
972
+ headers: { 'Content-Type': 'application/json' },
973
+ body: JSON.stringify({ sessionId, cronExpr, prompt }),
974
+ })
975
+ const data = (await resp.json()) as { ok: boolean; data?: { id: number }; error?: string }
976
+ if (!data.ok) {
977
+ console.error(`Error: ${data.error || 'Unknown error'}`)
978
+ process.exit(1)
979
+ }
980
+ console.log(`Schedule #${data.data!.id} created (cron: ${cronExpr}).`)
981
+ } catch (err) {
982
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
983
+ process.exit(1)
984
+ }
985
+ process.exit(0)
986
+ }
987
+
988
+ // ── mipham schedule remove <id> ─────────────────────────────────
989
+ if (subcmd === 'remove') {
990
+ const scheduleId = args[2]
991
+ if (!scheduleId) {
992
+ console.error('Usage: mipham schedule remove <id>')
993
+ process.exit(1)
994
+ }
995
+
996
+ try {
997
+ const resp = await fetch(`${baseUrl}/api/v1/schedules/${scheduleId}`, { method: 'DELETE' })
998
+ const data = (await resp.json()) as { ok: boolean; error?: string }
999
+ if (!data.ok) {
1000
+ console.error(`Error: ${data.error || 'Unknown error'}`)
1001
+ process.exit(1)
1002
+ }
1003
+ console.log(`Schedule #${scheduleId} removed.`)
1004
+ } catch (err) {
1005
+ console.error(`Failed to connect to daemon at port ${port}: ${String(err)}`)
1006
+ process.exit(1)
1007
+ }
1008
+ process.exit(0)
1009
+ }
1010
+
1011
+ console.error(`Unknown schedule command: mipham schedule ${subcmd}`)
1012
+ console.error('Usage: mipham schedule list|add|remove [options]')
1013
+ process.exit(1)
1014
+ }
1015
+
1016
+ // ── Token CLI: manage daemon API tokens ────────────────────────────────────────
1017
+
1018
+ async function runTokenCLI(): Promise<boolean> {
1019
+ const args = process.argv.slice(2)
1020
+ if (args[0] !== 'token') return false
1021
+
1022
+ const { join } = await import('node:path')
1023
+ const { readFileSync, existsSync } = await import('node:fs')
1024
+ const { homedir } = await import('node:os')
1025
+ const { rotateToken, listTokens } = await import('../src/daemon/auth')
1026
+
1027
+ const tokenPath = join(homedir(), '.mipham', 'daemon.token')
1028
+
1029
+ const subcmd = args[1]
1030
+
1031
+ // ── mipham token rotate ──────────────────────────────
1032
+ if (subcmd === 'rotate') {
1033
+ if (!existsSync(tokenPath)) {
1034
+ console.error('No token file found. Start the daemon first with: mipham daemon start')
1035
+ process.exit(1)
1036
+ }
1037
+ const oldTokens = listTokens(tokenPath)
1038
+ const newToken = rotateToken(tokenPath)
1039
+ console.log(`Token rotated successfully.`)
1040
+ console.log(` Old token: ${oldTokens[0]?.slice(0, 16) ?? 'none'}...`)
1041
+ console.log(` New token: ${newToken.slice(0, 16)}...`)
1042
+ console.log(` Full token: ${newToken}`)
1043
+ process.exit(0)
1044
+ }
1045
+
1046
+ // ── mipham token show ────────────────────────────────
1047
+ if (subcmd === 'show') {
1048
+ const tokens = listTokens(tokenPath)
1049
+ if (tokens.length === 0) {
1050
+ console.log('No token found. Start the daemon first with: mipham daemon start')
1051
+ } else {
1052
+ for (const t of tokens) {
1053
+ console.log(t)
1054
+ }
1055
+ }
1056
+ process.exit(0)
1057
+ }
1058
+
1059
+ console.error(`Unknown token command: mipham token ${subcmd}`)
1060
+ console.error('Usage: mipham token rotate|show')
1061
+ process.exit(1)
1062
+ }
1063
+
315
1064
  async function main() {
316
1065
  // ── Disable terminal special characters ──────────────────────────────────
317
1066
  // Terminal intercepts Ctrl+S (XOFF), Ctrl+T (SIGINFO/status), Ctrl+R (rprnt)
@@ -380,6 +1129,15 @@ Usage:
380
1129
  mipham Launch interactive CLI
381
1130
  mipham update Update to the latest version
382
1131
  mipham upgrade Same as 'mipham update'
1132
+ mipham daemon <cmd> Daemon lifecycle (start, stop, status, restart)
1133
+ mipham attach [id] Attach to a daemon session (--latest for recent)
1134
+ mipham agents List all agents
1135
+ mipham agent <id> Show agent detail
1136
+ mipham agent message <id> Send message to an agent
1137
+ mipham agent stop <id> Stop a running agent
1138
+ mipham goal <cmd> Goal management (list, add, done, pause)
1139
+ mipham schedule <cmd> Schedule management (list, add, remove)
1140
+ mipham token <cmd> Token management (show, rotate)
383
1141
  mipham plugin <cmd> Plugin management (install, list, remove, etc.)
384
1142
  mipham workflow <cmd> Workflow orchestration (run, list, resume, etc.)
385
1143
  mipham --version Print version and exit
@@ -398,6 +1156,30 @@ npm: https://www.npmjs.com/package/@miphamai/cli`)
398
1156
  const handledUpdate = await runUpdate()
399
1157
  if (handledUpdate) return
400
1158
 
1159
+ // ── Daemon commands ───────────────────────────────────────────────────────
1160
+ const handledDaemon = await runDaemonCLI()
1161
+ if (handledDaemon) return
1162
+
1163
+ // ── Attach commands ───────────────────────────────────────────────────────
1164
+ const handledAttach = await runAttachCLI()
1165
+ if (handledAttach) return
1166
+
1167
+ // ── Agent commands ────────────────────────────────────────────────────────
1168
+ const handledAgent = await runAgentCLI()
1169
+ if (handledAgent) return
1170
+
1171
+ // ── Goal commands ──────────────────────────────────────────────────────────
1172
+ const handledGoal = await runGoalCLI()
1173
+ if (handledGoal) return
1174
+
1175
+ // ── Schedule commands ──────────────────────────────────────────────────────
1176
+ const handledSchedule = await runScheduleCLI()
1177
+ if (handledSchedule) return
1178
+
1179
+ // ── Token commands ──────────────────────────────────────────────────────────
1180
+ const handledToken = await runTokenCLI()
1181
+ if (handledToken) return
1182
+
401
1183
  // Check for plugin subcommands first
402
1184
  const handledPlugin = await runPluginCLI()
403
1185
  if (handledPlugin) return
@@ -409,7 +1191,20 @@ npm: https://www.npmjs.com/package/@miphamai/cli`)
409
1191
  // ── Unknown command detection ──────────────────────────────────────────────
410
1192
  // After all known subcommands are checked, any remaining positional argument
411
1193
  // is probably a typo. Show error + suggestions instead of silently launching CLI.
412
- const KNOWN_COMMANDS = ['update', 'upgrade', 'plugin', 'workflow', 'help']
1194
+ const KNOWN_COMMANDS = [
1195
+ 'update',
1196
+ 'upgrade',
1197
+ 'plugin',
1198
+ 'workflow',
1199
+ 'daemon',
1200
+ 'attach',
1201
+ 'agents',
1202
+ 'agent',
1203
+ 'goal',
1204
+ 'schedule',
1205
+ 'token',
1206
+ 'help',
1207
+ ]
413
1208
  const firstArg = process.argv.slice(2).find((a) => !a.startsWith('-'))
414
1209
  if (firstArg) {
415
1210
  // Levenshtein distance to find closest match