@miphamai/cli 0.3.0 → 0.5.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.
Files changed (74) hide show
  1. package/bin/mipham.ts +188 -0
  2. package/dist/mipham +0 -0
  3. package/package.json +9 -9
  4. package/skills/mipham/om-artifact.mipham-skill.md +69 -0
  5. package/skills/mipham/om-model-optimize.mipham-skill.md +9 -6
  6. package/skills/mipham/om-security.mipham-skill.md +11 -8
  7. package/skills/standard/doc-generator.SKILL.md +6 -1
  8. package/skills/standard/github-ops.SKILL.md +12 -7
  9. package/skills/standard/memory.SKILL.md +1 -0
  10. package/skills/standard/self-review.SKILL.md +1 -0
  11. package/skills/standard/superpower.SKILL.md +7 -7
  12. package/skills/standard/web-search.SKILL.md +3 -0
  13. package/src/agent/agent-context.ts +56 -0
  14. package/src/agent/agent-registry.ts +134 -0
  15. package/src/agent/sub-agent.ts +73 -89
  16. package/src/agent/types.ts +39 -0
  17. package/src/agent-view/agent-view-manager.ts +217 -0
  18. package/src/agent-view/dashboard.tsx +218 -0
  19. package/src/agent-view/session-peek.tsx +72 -0
  20. package/src/agent-view/session-row.tsx +70 -0
  21. package/src/artifacts/manifest.ts +101 -0
  22. package/src/artifacts/server.ts +374 -0
  23. package/src/artifacts/versioning.ts +127 -0
  24. package/src/core/context-compact.ts +50 -0
  25. package/src/core/context-drain.ts +54 -0
  26. package/src/core/context-microcompact.ts +132 -0
  27. package/src/core/context-snip.ts +74 -0
  28. package/src/core/context-token.ts +108 -0
  29. package/src/core/context.ts +169 -0
  30. package/src/core/engine.ts +207 -58
  31. package/src/core/hooks-config.ts +52 -0
  32. package/src/core/hooks-executor.ts +113 -0
  33. package/src/core/hooks.ts +90 -30
  34. package/src/core/instructions.ts +12 -1
  35. package/src/core/memory/memory-loader.ts +23 -0
  36. package/src/core/memory/memory-manager.ts +192 -0
  37. package/src/core/memory/memory-writer.ts +72 -0
  38. package/src/core/permission-config.ts +34 -0
  39. package/src/core/permission-rules.ts +66 -0
  40. package/src/core/permission.ts +224 -34
  41. package/src/index.tsx +30 -2
  42. package/src/mcp/transport.ts +42 -5
  43. package/src/plugin/plugin-loader.ts +36 -0
  44. package/src/plugin/plugin-manager.ts +125 -0
  45. package/src/plugin/plugin-validator.ts +41 -0
  46. package/src/providers/fetch-utils.ts +1 -3
  47. package/src/providers/openai-compat.ts +2 -11
  48. package/src/shared/constants.ts +8 -1
  49. package/src/shared/types.ts +82 -2
  50. package/src/skills/fork-executor.ts +40 -0
  51. package/src/skills/loader.ts +48 -2
  52. package/src/tools/agent/agent.ts +20 -11
  53. package/src/tools/agent/skill.ts +32 -2
  54. package/src/tools/artifact/artifact.ts +144 -0
  55. package/src/tools/computer/app-launcher.ts +39 -0
  56. package/src/tools/computer/browser.ts +48 -0
  57. package/src/tools/computer/computer-use.ts +88 -0
  58. package/src/tools/computer/playwright.d.ts +7 -0
  59. package/src/tools/computer/screenshot.ts +57 -0
  60. package/src/tools/index.ts +6 -0
  61. package/src/ui/app.tsx +20 -7
  62. package/src/ui/chat.tsx +9 -5
  63. package/src/ui/commands.ts +185 -40
  64. package/src/ui/input.tsx +203 -27
  65. package/src/ui/picker.tsx +2 -5
  66. package/src/ui/vim-motions.ts +96 -0
  67. package/src/workflow/budget.ts +33 -0
  68. package/src/workflow/journal.ts +105 -0
  69. package/src/workflow/primitives/agent.ts +51 -0
  70. package/src/workflow/primitives/parallel.ts +8 -0
  71. package/src/workflow/primitives/phase.ts +19 -0
  72. package/src/workflow/primitives/pipeline.ts +24 -0
  73. package/src/workflow/runtime.ts +87 -0
  74. package/src/workflow/sandbox.ts +75 -0
@@ -0,0 +1,218 @@
1
+ /**
2
+ * AgentViewDashboard — Ink TUI for background agent session management.
3
+ *
4
+ * Displays sessions grouped by status (needs-input / working / completed / failed),
5
+ * with j/k navigation, Space peek, Enter attach, and Esc exit.
6
+ *
7
+ * Usage:
8
+ * mipham agents (from CLI)
9
+ * /agents (from slash command within a running session)
10
+ */
11
+ import React, { useState, useCallback, useMemo } from 'react'
12
+ import { Box, Text, useInput } from 'ink'
13
+ import { AgentViewManager, type AgentSession, type SessionStatus } from './agent-view-manager'
14
+ import { SessionRow } from './session-row'
15
+ import { SessionPeek } from './session-peek'
16
+
17
+ interface DashboardProps {
18
+ manager: AgentViewManager
19
+ onAttach?: (session: AgentSession) => void
20
+ onExit: () => void
21
+ }
22
+
23
+ const STATUS_HEADERS: Record<string, { label: string; color: string }> = {
24
+ 'needs-input': { label: 'Needs Input', color: 'yellow' },
25
+ working: { label: 'Working', color: 'cyan' },
26
+ completed: { label: 'Completed', color: 'green' },
27
+ failed: { label: 'Failed', color: 'red' },
28
+ }
29
+
30
+ export function AgentViewDashboard({ manager, onAttach, onExit }: DashboardProps) {
31
+ const [selectedIndex, setSelectedIndex] = useState(0)
32
+ const [peekingSessionId, setPeekingSessionId] = useState<string | null>(null)
33
+
34
+ // Build a flat list of sessions in group order, with group headers
35
+ const flatList = useMemo(() => {
36
+ const groups = manager.groupByStatus()
37
+ const result: Array<
38
+ | { type: 'header'; status: string; label: string; color: string; count: number }
39
+ | { type: 'session'; session: AgentSession }
40
+ > = []
41
+
42
+ const statusOrder: Array<keyof typeof STATUS_HEADERS> = [
43
+ 'working',
44
+ 'needs-input',
45
+ 'completed',
46
+ 'failed',
47
+ ]
48
+
49
+ for (const _status of statusOrder) {
50
+ const status = _status as SessionStatus
51
+ const sessions = groups[status] ?? []
52
+ result.push({
53
+ type: 'header',
54
+ status,
55
+ label: STATUS_HEADERS[status]!.label,
56
+ color: STATUS_HEADERS[status]!.color,
57
+ count: sessions.length,
58
+ })
59
+
60
+ for (const session of sessions) {
61
+ result.push({ type: 'session', session })
62
+ }
63
+ }
64
+
65
+ return result
66
+ }, [manager])
67
+
68
+ // Flatten sessions only for navigation (skip headers)
69
+ const sessionsOnly = useMemo(
70
+ () =>
71
+ flatList.filter((item) => item.type === 'session') as Array<{
72
+ type: 'session'
73
+ session: AgentSession
74
+ }>,
75
+ [flatList],
76
+ )
77
+
78
+ const handleAttach = useCallback(
79
+ (sessionId: string) => {
80
+ const session = manager.attach(sessionId)
81
+ if (session && onAttach) {
82
+ onAttach(session)
83
+ }
84
+ },
85
+ [manager, onAttach],
86
+ )
87
+
88
+ useInput((input, key) => {
89
+ if (key.escape) {
90
+ if (peekingSessionId) {
91
+ setPeekingSessionId(null)
92
+ return
93
+ }
94
+ onExit()
95
+ return
96
+ }
97
+
98
+ if (input === 'j') {
99
+ setSelectedIndex((prev) => Math.min(prev + 1, sessionsOnly.length - 1))
100
+ setPeekingSessionId(null)
101
+ return
102
+ }
103
+
104
+ if (input === 'k') {
105
+ setSelectedIndex((prev) => Math.max(prev - 1, 0))
106
+ setPeekingSessionId(null)
107
+ return
108
+ }
109
+
110
+ // Space — toggle peek
111
+ if (input === ' ') {
112
+ if (sessionsOnly.length === 0) return
113
+ const current = sessionsOnly[selectedIndex]
114
+ if (!current) return
115
+ setPeekingSessionId(peekingSessionId === current.session.id ? null : current.session.id)
116
+ return
117
+ }
118
+
119
+ // Enter — attach to selected session
120
+ if (key.return) {
121
+ if (sessionsOnly.length === 0) return
122
+ const current = sessionsOnly[selectedIndex]
123
+ if (!current) return
124
+ if (peekingSessionId) {
125
+ handleAttach(current.session.id)
126
+ } else {
127
+ handleAttach(current.session.id)
128
+ }
129
+ return
130
+ }
131
+ })
132
+
133
+ // Compute the peek data for the currently peeking session
134
+ const peekData = useMemo(() => {
135
+ if (!peekingSessionId) return null
136
+ return manager.peek(peekingSessionId) ?? null
137
+ }, [manager, peekingSessionId])
138
+
139
+ const totalSessions = sessionsOnly.length
140
+ const counts = manager.countByStatus()
141
+
142
+ return (
143
+ <Box flexDirection="column" padding={1} height="100%">
144
+ {/* Header */}
145
+ <Box marginBottom={1} flexDirection="column">
146
+ <Box>
147
+ <Text bold color="cyan">
148
+ Agent View
149
+ </Text>
150
+ <Text dimColor> — Background Agent Dashboard</Text>
151
+ </Box>
152
+ <Box>
153
+ <Text dimColor>
154
+ {totalSessions} session{totalSessions !== 1 ? 's' : ''}
155
+ {' · '}
156
+ <Text color="cyan">{counts.working} working</Text>
157
+ {' · '}
158
+ <Text color="yellow">{counts['needs-input']} input</Text>
159
+ {' · '}
160
+ <Text color="green">{counts.completed} done</Text>
161
+ {' · '}
162
+ <Text color="red">{counts.failed} failed</Text>
163
+ </Text>
164
+ </Box>
165
+ <Box>
166
+ <Text dimColor>j/k navigate · Space peek · Enter attach · Esc back</Text>
167
+ </Box>
168
+ </Box>
169
+
170
+ {/* Divider */}
171
+ <Box marginBottom={1}>
172
+ <Text dimColor>{'─'.repeat(70)}</Text>
173
+ </Box>
174
+
175
+ {/* Empty state */}
176
+ {totalSessions === 0 ? (
177
+ <Box flexDirection="column" paddingY={2} paddingLeft={2}>
178
+ <Text dimColor>No background agents.</Text>
179
+ <Text dimColor>
180
+ Use the Agent tool or type &quot;run this in background&quot; to spawn one.
181
+ </Text>
182
+ </Box>
183
+ ) : (
184
+ <Box flexDirection="column">
185
+ {/* Session list with group headers */}
186
+ {flatList.map((item, flatIdx) => {
187
+ if (item.type === 'header') {
188
+ return (
189
+ <Box key={`h-${item.status}`} marginY={1}>
190
+ <Text bold color={item.color}>
191
+ {' '}
192
+ {item.label} ({item.count})
193
+ </Text>
194
+ </Box>
195
+ )
196
+ }
197
+
198
+ // Map this session's position in sessionsOnly to selectedIndex
199
+ const sessionIdx = sessionsOnly.findIndex((s) => s.session.id === item.session.id)
200
+
201
+ return (
202
+ <SessionRow
203
+ key={item.session.id}
204
+ session={item.session}
205
+ isSelected={sessionIdx === selectedIndex}
206
+ />
207
+ )
208
+ })}
209
+ </Box>
210
+ )}
211
+
212
+ {/* Peek panel (shown below the list when peeking) */}
213
+ {peekData && (
214
+ <SessionPeek session={peekData.session} recentMessages={peekData.recentMessages} />
215
+ )}
216
+ </Box>
217
+ )
218
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * SessionPeek — a popup panel that shows the last few messages of
3
+ * a selected agent session. Invoked by pressing Space on a session row.
4
+ */
5
+ import React from 'react'
6
+ import { Box, Text } from 'ink'
7
+ import type { AgentSession, SessionMessage } from './agent-view-manager'
8
+
9
+ interface SessionPeekProps {
10
+ session: AgentSession
11
+ recentMessages: SessionMessage[]
12
+ }
13
+
14
+ const ROLE_COLORS: Record<string, string> = {
15
+ user: 'green',
16
+ assistant: 'cyan',
17
+ system: 'yellow',
18
+ }
19
+
20
+ export function SessionPeek({ session, recentMessages }: SessionPeekProps) {
21
+ return (
22
+ <Box flexDirection="column" borderStyle="single" borderColor="blue" padding={1} marginY={1}>
23
+ {/* Header */}
24
+ <Box marginBottom={1}>
25
+ <Text bold color="blue">
26
+ {' '}
27
+ Peek: {session.id}{' '}
28
+ </Text>
29
+ <Text dimColor>
30
+ — {session.title} · {session.provider}/{session.model}
31
+ </Text>
32
+ </Box>
33
+
34
+ {/* Divider */}
35
+ <Box marginBottom={1}>
36
+ <Text dimColor>{'─'.repeat(60)}</Text>
37
+ </Box>
38
+
39
+ {/* Messages */}
40
+ {recentMessages.length === 0 ? (
41
+ <Box paddingLeft={2}>
42
+ <Text dimColor>(no messages yet)</Text>
43
+ </Box>
44
+ ) : (
45
+ recentMessages.map((msg, i) => (
46
+ <Box key={i} flexDirection="column" marginBottom={1}>
47
+ <Text color={ROLE_COLORS[msg.role] || 'white'} bold>
48
+ [{msg.role}]
49
+ </Text>
50
+ <Box paddingLeft={4}>
51
+ <Text dimColor>
52
+ {msg.content.length > 120 ? msg.content.slice(0, 120) + '...' : msg.content}
53
+ </Text>
54
+ </Box>
55
+ </Box>
56
+ ))
57
+ )}
58
+
59
+ {/* Footer */}
60
+ <Box marginTop={1}>
61
+ <Text dimColor>{'─'.repeat(60)}</Text>
62
+ </Box>
63
+ <Box>
64
+ <Text dimColor>
65
+ {' '}
66
+ {recentMessages.length} message(s) · Total: {session.messages.length} · Status:{' '}
67
+ {session.status}
68
+ </Text>
69
+ </Box>
70
+ </Box>
71
+ )
72
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * SessionRow — a single row in the Agent View dashboard.
3
+ * Shows provider, model, task, and elapsed time for one background agent session.
4
+ */
5
+ import React from 'react'
6
+ import { Box, Text } from 'ink'
7
+ import type { AgentSession } from './agent-view-manager'
8
+
9
+ interface SessionRowProps {
10
+ session: AgentSession
11
+ isSelected: boolean
12
+ }
13
+
14
+ function formatElapsed(ms: number): string {
15
+ if (ms < 1000) return '<1s'
16
+ const seconds = Math.floor(ms / 1000)
17
+ if (seconds < 60) return `${seconds}s`
18
+ const minutes = Math.floor(seconds / 60)
19
+ const remainingSecs = seconds % 60
20
+ if (minutes < 60) return `${minutes}m ${remainingSecs}s`
21
+ const hours = Math.floor(minutes / 60)
22
+ const remainingMin = minutes % 60
23
+ return `${hours}h ${remainingMin}m`
24
+ }
25
+
26
+ const STATUS_COLORS: Record<string, string> = {
27
+ 'needs-input': 'yellow',
28
+ working: 'cyan',
29
+ completed: 'green',
30
+ failed: 'red',
31
+ }
32
+
33
+ const STATUS_LABELS: Record<string, string> = {
34
+ 'needs-input': '[INPUT]',
35
+ working: '[WORK] ',
36
+ completed: '[DONE] ',
37
+ failed: '[FAIL] ',
38
+ }
39
+
40
+ export function SessionRow({ session, isSelected }: SessionRowProps) {
41
+ const color = STATUS_COLORS[session.status] || 'white'
42
+ const label = STATUS_LABELS[session.status] || ''
43
+ const prefix = isSelected ? '❯' : ' '
44
+
45
+ const taskPreview = session.task.length > 60 ? session.task.slice(0, 60) + '...' : session.task
46
+
47
+ return (
48
+ <Box flexDirection="column">
49
+ <Box>
50
+ <Text color={isSelected ? 'blue' : undefined} bold={isSelected}>
51
+ {prefix}{' '}
52
+ </Text>
53
+ <Text color={color}>{label}</Text>
54
+ <Text> </Text>
55
+ <Text bold={isSelected}>{session.id}</Text>
56
+ <Text dimColor>
57
+ {' '}
58
+ · {session.provider}/{session.model}
59
+ </Text>
60
+ <Text dimColor> · {formatElapsed(session.elapsedMs)}</Text>
61
+ </Box>
62
+ <Box paddingLeft={isSelected ? 4 : 3}>
63
+ <Text dimColor={!isSelected} color={isSelected ? 'white' : undefined}>
64
+ {taskPreview}
65
+ </Text>
66
+ {session.title !== session.task.slice(0, 60) && <Text dimColor> — {session.title}</Text>}
67
+ </Box>
68
+ </Box>
69
+ )
70
+ }
@@ -0,0 +1,101 @@
1
+ import {
2
+ readFileSync,
3
+ writeFileSync,
4
+ existsSync,
5
+ mkdirSync,
6
+ renameSync,
7
+ copyFileSync,
8
+ unlinkSync,
9
+ } from 'node:fs'
10
+ import { join } from 'node:path'
11
+ import type { ArtifactManifest, ArtifactEntry } from '../shared/types'
12
+
13
+ /**
14
+ * Read the artifact manifest from disk, or return an empty one if it doesn't exist.
15
+ */
16
+ export function readManifest(dir: string): ArtifactManifest {
17
+ const path = join(dir, 'index.json')
18
+ if (!existsSync(path)) {
19
+ return { version: 1, artifacts: [] }
20
+ }
21
+ try {
22
+ return JSON.parse(readFileSync(path, 'utf-8'))
23
+ } catch {
24
+ return { version: 1, artifacts: [] }
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Write the manifest to disk, creating parent directories as needed.
30
+ */
31
+ export function writeManifest(dir: string, manifest: ArtifactManifest): void {
32
+ mkdirSync(dir, { recursive: true })
33
+ writeFileSync(join(dir, 'index.json'), JSON.stringify(manifest, null, 2), 'utf-8')
34
+ }
35
+
36
+ /**
37
+ * Add an entry to the manifest and persist it.
38
+ * If an entry with the same name already exists, it is replaced.
39
+ */
40
+ export function addToManifest(dir: string, entry: ArtifactEntry, port?: number): ArtifactManifest {
41
+ const manifest = readManifest(dir)
42
+ if (port !== undefined) manifest.port = port
43
+
44
+ const idx = manifest.artifacts.findIndex((a) => a.name === entry.name)
45
+ if (idx >= 0) {
46
+ manifest.artifacts[idx] = entry
47
+ } else {
48
+ manifest.artifacts.push(entry)
49
+ }
50
+
51
+ writeManifest(dir, manifest)
52
+ return manifest
53
+ }
54
+
55
+ /**
56
+ * Get all artifacts for a specific session.
57
+ */
58
+ export function getSessionArtifacts(dir: string, sessionId: string): ArtifactEntry[] {
59
+ const manifest = readManifest(dir)
60
+ return manifest.artifacts.filter((a) => a.sessionId === sessionId)
61
+ }
62
+
63
+ /**
64
+ * Archive an existing artifact file by renaming it with a version tag.
65
+ * e.g. dashboard.html → dashboard.v1.html, dashboard.v1.html → dashboard.v2.html.
66
+ *
67
+ * Returns the version tag assigned to the archived file.
68
+ */
69
+ export function archiveVersion(dir: string, entry: ArtifactEntry): string {
70
+ const manifest = readManifest(dir)
71
+ const versionCount = (entry.versionCount || 1) + 1
72
+ const ext = entry.type === 'svg' ? '.svg' : '.html'
73
+ const versionTag = `v${versionCount}`
74
+
75
+ // Rename the current file to a versioned copy
76
+ const baseName = entry.name
77
+ const currentPath = join(dir, entry.sessionId, `${baseName}${ext}`)
78
+ const archivedPath = join(dir, entry.sessionId, `${baseName}.${versionTag}${ext}`)
79
+
80
+ if (existsSync(currentPath)) {
81
+ try {
82
+ renameSync(currentPath, archivedPath)
83
+ } catch {
84
+ // If rename fails (e.g. cross-device), copy instead
85
+ copyFileSync(currentPath, archivedPath)
86
+ unlinkSync(currentPath)
87
+ }
88
+ }
89
+
90
+ // Update manifest entry
91
+ const artifact = manifest.artifacts.find((a) => a.name === entry.name)
92
+ if (artifact) {
93
+ const versions = artifact.versions || ['v1']
94
+ versions.push(versionTag)
95
+ artifact.versions = versions
96
+ artifact.versionCount = versionCount
97
+ }
98
+
99
+ writeManifest(dir, manifest)
100
+ return versionTag
101
+ }