@miphamai/cli 0.31.1 → 0.32.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/bin/daemon.ts +40 -0
- package/bin/mipham.ts +796 -1
- package/package.json +1 -1
- package/src/agent/background-registry.ts +80 -1
- package/src/daemon/agent-manager.ts +110 -0
- package/src/daemon/attach-protocol.ts +74 -0
- package/src/daemon/auth.ts +104 -0
- package/src/daemon/cors.ts +68 -0
- package/src/daemon/database.ts +449 -0
- package/src/daemon/goal-manager.ts +53 -0
- package/src/daemon/index.ts +307 -0
- package/src/daemon/message-bus.ts +84 -0
- package/src/daemon/rate-limiter.ts +70 -0
- package/src/daemon/remote-engine.ts +434 -0
- package/src/daemon/schedule-manager.ts +159 -0
- package/src/daemon/server.ts +632 -0
- package/src/daemon/session-manager.ts +45 -0
- package/src/daemon/session-worker.ts +418 -0
- package/src/daemon/types.ts +98 -0
- package/src/daemon/worker-pool.ts +174 -0
- package/src/index.tsx +44 -4
- package/src/ui/app.tsx +6 -2
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 = [
|
|
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
|