@miphamai/cli 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.jpg +0 -0
- package/bin/mipham +51 -0
- package/bin/mipham.ts +8 -0
- package/package.json +59 -0
- package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
- package/skills/mipham/om-security.mipham-skill.md +21 -0
- package/skills/standard/code-review.SKILL.md +116 -0
- package/skills/standard/compassionate-communication.SKILL.md +80 -0
- package/skills/standard/doc-generator.SKILL.md +21 -0
- package/skills/standard/github-ops.SKILL.md +22 -0
- package/skills/standard/memory.SKILL.md +22 -0
- package/skills/standard/mipham-code-setup.SKILL.md +113 -0
- package/skills/standard/security-review.SKILL.md +122 -0
- package/skills/standard/self-review.SKILL.md +20 -0
- package/skills/standard/superpower.SKILL.md +19 -0
- package/skills/standard/tdd.SKILL.md +20 -0
- package/skills/standard/web-search.SKILL.md +23 -0
- package/src/agent/sub-agent.ts +122 -0
- package/src/config/defaults.ts +10 -0
- package/src/config/loader.ts +30 -0
- package/src/core/context.ts +135 -0
- package/src/core/engine.ts +193 -0
- package/src/core/hooks.ts +116 -0
- package/src/core/instructions.ts +126 -0
- package/src/core/permission.ts +54 -0
- package/src/core/session-store.ts +136 -0
- package/src/index.tsx +93 -0
- package/src/mcp/client.ts +170 -0
- package/src/mcp/protocol.ts +104 -0
- package/src/mcp/transport.ts +169 -0
- package/src/mcp/types.ts +112 -0
- package/src/providers/anthropic.ts +286 -0
- package/src/providers/bootstrap.ts +28 -0
- package/src/providers/openai-compat.ts +158 -0
- package/src/providers/registry.ts +70 -0
- package/src/security/path.ts +90 -0
- package/src/security/url.ts +96 -0
- package/src/shared/constants.ts +368 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/types.ts +161 -0
- package/src/skills/loader.ts +109 -0
- package/src/skills/mipham/runtime.ts +63 -0
- package/src/skills/standard/runtime.ts +59 -0
- package/src/tools/agent/agent.ts +51 -0
- package/src/tools/agent/memory.ts +51 -0
- package/src/tools/agent/plan.ts +87 -0
- package/src/tools/agent/skill.ts +58 -0
- package/src/tools/exec/bash.ts +111 -0
- package/src/tools/exec/git.ts +63 -0
- package/src/tools/exec/task.ts +69 -0
- package/src/tools/file/edit.ts +57 -0
- package/src/tools/file/glob.ts +29 -0
- package/src/tools/file/grep.ts +52 -0
- package/src/tools/file/read.ts +38 -0
- package/src/tools/file/write.ts +27 -0
- package/src/tools/index.ts +126 -0
- package/src/tools/network/web-fetch.ts +61 -0
- package/src/tools/network/web-search.ts +32 -0
- package/src/tools/system/config.ts +68 -0
- package/src/tools/system/mcp.ts +75 -0
- package/src/ui/app.tsx +317 -0
- package/src/ui/chat.tsx +76 -0
- package/src/ui/commands.ts +2232 -0
- package/src/ui/input.tsx +83 -0
- package/src/ui/picker.tsx +209 -0
package/src/ui/app.tsx
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react'
|
|
2
|
+
import { Box, Text, useInput } from 'ink'
|
|
3
|
+
import type { QueryEngine } from '../core/engine'
|
|
4
|
+
import type { MiphamConfig } from '../shared/index.ts'
|
|
5
|
+
import type { SkillsLoader } from '../skills/loader'
|
|
6
|
+
import { ChatPanel } from './chat'
|
|
7
|
+
import { InputBar } from './input'
|
|
8
|
+
import { ModelPicker } from './picker'
|
|
9
|
+
import {
|
|
10
|
+
getCommand,
|
|
11
|
+
looksLikeSlashCommand,
|
|
12
|
+
parseSlashCommand,
|
|
13
|
+
handleSwitch,
|
|
14
|
+
type CommandContext,
|
|
15
|
+
} from './commands'
|
|
16
|
+
|
|
17
|
+
interface AppProps {
|
|
18
|
+
engine: QueryEngine
|
|
19
|
+
config: MiphamConfig
|
|
20
|
+
initialProvider?: string
|
|
21
|
+
initialModel?: string
|
|
22
|
+
lang?: string
|
|
23
|
+
skillsLoader?: SkillsLoader
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ChatMessage {
|
|
27
|
+
role: 'user' | 'assistant' | 'system'
|
|
28
|
+
content: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const VERSION = '0.2.0'
|
|
32
|
+
|
|
33
|
+
type PermissionMode = 'auto' | 'ask' | 'bypass'
|
|
34
|
+
|
|
35
|
+
const PERMISSION_MODES: PermissionMode[] = ['auto', 'ask', 'bypass']
|
|
36
|
+
const PERMISSION_LABELS: Record<PermissionMode, string> = {
|
|
37
|
+
auto: 'auto mode on',
|
|
38
|
+
ask: 'ask mode (confirm each action)',
|
|
39
|
+
bypass: 'bypass mode (skip all checks)',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function App({
|
|
43
|
+
engine,
|
|
44
|
+
config,
|
|
45
|
+
initialProvider,
|
|
46
|
+
initialModel,
|
|
47
|
+
lang,
|
|
48
|
+
skillsLoader,
|
|
49
|
+
}: AppProps) {
|
|
50
|
+
const [messages, setMessages] = useState<ChatMessage[]>([])
|
|
51
|
+
const [isLoading, setIsLoading] = useState(false)
|
|
52
|
+
const [providerId, setProviderId] = useState(initialProvider || config.defaultProvider)
|
|
53
|
+
const [modelId, setModelId] = useState(initialModel || config.defaultModel)
|
|
54
|
+
const [pickerOpen, setPickerOpen] = useState(false)
|
|
55
|
+
const [sessionTitle, setSessionTitle] = useState('')
|
|
56
|
+
const [fastMode, setFastMode] = useState(false)
|
|
57
|
+
const [effort, setEffort] = useState('high')
|
|
58
|
+
const [focusMode, setFocusMode] = useState(false)
|
|
59
|
+
const [goalText, setGoalText] = useState('')
|
|
60
|
+
const [permissionMode, setPermissionMode] = useState<PermissionMode>('auto')
|
|
61
|
+
|
|
62
|
+
const mkCtx = useCallback(
|
|
63
|
+
(): CommandContext => ({
|
|
64
|
+
engine,
|
|
65
|
+
config,
|
|
66
|
+
providerId,
|
|
67
|
+
modelId,
|
|
68
|
+
version: VERSION,
|
|
69
|
+
setSessionTitle: (title: string) => setSessionTitle(title),
|
|
70
|
+
setFastMode: (on: boolean) => setFastMode(on),
|
|
71
|
+
setEffort: (level: string) => setEffort(level),
|
|
72
|
+
setFocusMode: (on: boolean) => setFocusMode(on),
|
|
73
|
+
setGoal: (text: string) => setGoalText(text),
|
|
74
|
+
skillsLoader,
|
|
75
|
+
}),
|
|
76
|
+
[engine, config, providerId, modelId, skillsLoader],
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
const handleSubmit = useCallback(
|
|
80
|
+
async (input: string) => {
|
|
81
|
+
if (!input.trim()) return
|
|
82
|
+
|
|
83
|
+
// ── Slash command dispatch ──
|
|
84
|
+
if (looksLikeSlashCommand(input)) {
|
|
85
|
+
const { command, args } = parseSlashCommand(input)
|
|
86
|
+
|
|
87
|
+
// /switch takes args, handled separately
|
|
88
|
+
if (command === '/switch') {
|
|
89
|
+
const result = await handleSwitch(mkCtx(), args)
|
|
90
|
+
setMessages((prev) => [
|
|
91
|
+
...prev,
|
|
92
|
+
{ role: 'user', content: input },
|
|
93
|
+
{ role: 'system', content: result.content },
|
|
94
|
+
])
|
|
95
|
+
if (result.nextProvider) setProviderId(result.nextProvider)
|
|
96
|
+
if (result.nextModel) setModelId(result.nextModel)
|
|
97
|
+
if (result.exit) process.exit(0)
|
|
98
|
+
return
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// /pick → open interactive model picker
|
|
102
|
+
if (command === '/pick' || command === '/model-picker') {
|
|
103
|
+
setPickerOpen(true)
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// /quit and /exit are special
|
|
108
|
+
if (command === '/exit' || command === '/quit') {
|
|
109
|
+
process.exit(0)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// /focus toggle
|
|
113
|
+
if (command === '/focus') {
|
|
114
|
+
const nextFocus = !focusMode
|
|
115
|
+
setFocusMode(nextFocus)
|
|
116
|
+
setMessages((prev) => [
|
|
117
|
+
...prev,
|
|
118
|
+
{ role: 'user', content: input },
|
|
119
|
+
{
|
|
120
|
+
role: 'system',
|
|
121
|
+
content: nextFocus
|
|
122
|
+
? '✓ Focus mode ON — showing only the most recent exchange. Type /focus again to show all.'
|
|
123
|
+
: '✓ Focus mode OFF — showing all messages.',
|
|
124
|
+
},
|
|
125
|
+
])
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const handler = getCommand(command)
|
|
130
|
+
if (handler) {
|
|
131
|
+
const result = await handler(mkCtx(), args)
|
|
132
|
+
setMessages((prev) => [
|
|
133
|
+
...prev,
|
|
134
|
+
{ role: 'user', content: input },
|
|
135
|
+
{ role: 'system', content: result.content },
|
|
136
|
+
])
|
|
137
|
+
if (result.clearMessages) setMessages([])
|
|
138
|
+
if (result.nextProvider) setProviderId(result.nextProvider)
|
|
139
|
+
if (result.nextModel) setModelId(result.nextModel)
|
|
140
|
+
if (result.exit) process.exit(0)
|
|
141
|
+
if (result.copyContent) {
|
|
142
|
+
// Copy to clipboard via pbcopy (macOS) or clip (Windows)
|
|
143
|
+
try {
|
|
144
|
+
const { execSync } = await import('node:child_process')
|
|
145
|
+
if (process.platform === 'darwin') {
|
|
146
|
+
execSync('pbcopy', { input: result.copyContent })
|
|
147
|
+
} else if (process.platform === 'win32') {
|
|
148
|
+
execSync('clip', { input: result.copyContent })
|
|
149
|
+
}
|
|
150
|
+
// Linux: xclip or wl-copy not attempted to avoid dependency issues
|
|
151
|
+
} catch {
|
|
152
|
+
// Silent fail — content is still displayed
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// Unknown slash command or handler returned: proceed to normal AI processing
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ── Normal message processing (AI chat) ──
|
|
161
|
+
setMessages((prev) => [...prev, { role: 'user', content: input }])
|
|
162
|
+
setIsLoading(true)
|
|
163
|
+
|
|
164
|
+
let assistantContent = ''
|
|
165
|
+
|
|
166
|
+
try {
|
|
167
|
+
for await (const chunk of engine.process(input)) {
|
|
168
|
+
if (chunk.type === 'text' && chunk.content) {
|
|
169
|
+
assistantContent += chunk.content
|
|
170
|
+
setMessages((prev) => {
|
|
171
|
+
const updated = [...prev]
|
|
172
|
+
const last = updated[updated.length - 1]
|
|
173
|
+
if (last?.role === 'assistant') {
|
|
174
|
+
last.content = assistantContent
|
|
175
|
+
} else {
|
|
176
|
+
updated.push({ role: 'assistant', content: assistantContent })
|
|
177
|
+
}
|
|
178
|
+
return updated
|
|
179
|
+
})
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (chunk.type === 'tool_use' && chunk.toolUse) {
|
|
183
|
+
setMessages((prev) => [
|
|
184
|
+
...prev,
|
|
185
|
+
{
|
|
186
|
+
role: 'system',
|
|
187
|
+
content: `🔧 Using tool: ${chunk.toolUse!.name}(${JSON.stringify(chunk.toolUse!.input)})`,
|
|
188
|
+
},
|
|
189
|
+
])
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (chunk.type === 'tool_result') {
|
|
193
|
+
const resultPreview =
|
|
194
|
+
chunk.content && chunk.content.length > 200
|
|
195
|
+
? chunk.content.slice(0, 200) + '...'
|
|
196
|
+
: chunk.content || '(empty)'
|
|
197
|
+
setMessages((prev) => [
|
|
198
|
+
...prev,
|
|
199
|
+
{ role: 'system', content: `📋 Tool result: ${resultPreview}` },
|
|
200
|
+
])
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (chunk.type === 'error') {
|
|
204
|
+
setMessages((prev) => [
|
|
205
|
+
...prev,
|
|
206
|
+
{ role: 'system', content: `❌ Error: ${chunk.error}` },
|
|
207
|
+
])
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
} catch (err) {
|
|
211
|
+
setMessages((prev) => [...prev, { role: 'system', content: `Error: ${String(err)}` }])
|
|
212
|
+
} finally {
|
|
213
|
+
setIsLoading(false)
|
|
214
|
+
// Auto-save checkpoint after each AI response
|
|
215
|
+
if (assistantContent) {
|
|
216
|
+
engine.getContext().saveCheckpoint('post-turn')
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
[engine, mkCtx],
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
useInput((_input, key) => {
|
|
224
|
+
// Global hotkeys
|
|
225
|
+
if (key.escape) {
|
|
226
|
+
if (pickerOpen) {
|
|
227
|
+
setPickerOpen(false)
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
process.exit(0)
|
|
231
|
+
}
|
|
232
|
+
// Ctrl+P → open model picker
|
|
233
|
+
if (_input === '\x10') {
|
|
234
|
+
setPickerOpen((prev) => !prev)
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
// Shift+Tab → cycle permission mode (auto → ask → bypass → auto)
|
|
238
|
+
if (key.shift && key.tab) {
|
|
239
|
+
setPermissionMode((prev) => {
|
|
240
|
+
const idx = PERMISSION_MODES.indexOf(prev)
|
|
241
|
+
const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
|
|
242
|
+
// Sync to engine
|
|
243
|
+
engine.getPermission().setDefaultLevel(next)
|
|
244
|
+
return next
|
|
245
|
+
})
|
|
246
|
+
return
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<Box flexDirection="column" padding={1} height="100%">
|
|
252
|
+
{/* Header — brand mark + status line */}
|
|
253
|
+
<Box marginBottom={1} flexDirection="column">
|
|
254
|
+
<Box flexDirection="row">
|
|
255
|
+
<Text bold color="cyan">
|
|
256
|
+
Mipham Code
|
|
257
|
+
</Text>
|
|
258
|
+
<Text dimColor> v0.2.0</Text>
|
|
259
|
+
{sessionTitle ? (
|
|
260
|
+
<Text color="yellow"> — {sessionTitle}</Text>
|
|
261
|
+
) : (
|
|
262
|
+
<Text dimColor> — MiphamAI</Text>
|
|
263
|
+
)}
|
|
264
|
+
</Box>
|
|
265
|
+
<Box flexDirection="row">
|
|
266
|
+
<Text dimColor>
|
|
267
|
+
{modelId} ({providerId}){fastMode && ' ⚡'}
|
|
268
|
+
{effort !== 'high' && ` 🧠${effort}`}
|
|
269
|
+
{focusMode && ' 🔍focus'}
|
|
270
|
+
</Text>
|
|
271
|
+
</Box>
|
|
272
|
+
<Box flexDirection="row">
|
|
273
|
+
<Text
|
|
274
|
+
color={
|
|
275
|
+
permissionMode === 'auto' ? 'green' : permissionMode === 'ask' ? 'yellow' : 'red'
|
|
276
|
+
}
|
|
277
|
+
>
|
|
278
|
+
● {PERMISSION_LABELS[permissionMode]}
|
|
279
|
+
</Text>
|
|
280
|
+
<Text dimColor> (Shift+Tab to cycle)</Text>
|
|
281
|
+
<Text dimColor> · Ctrl+P pick · /help · Esc exit</Text>
|
|
282
|
+
</Box>
|
|
283
|
+
{goalText && (
|
|
284
|
+
<Box>
|
|
285
|
+
<Text color="green">🎯 Goal: {goalText}</Text>
|
|
286
|
+
</Box>
|
|
287
|
+
)}
|
|
288
|
+
</Box>
|
|
289
|
+
|
|
290
|
+
{/* Chat panel */}
|
|
291
|
+
<ChatPanel messages={messages} focusMode={focusMode} />
|
|
292
|
+
|
|
293
|
+
{/* Model picker (replaces input when open) */}
|
|
294
|
+
{pickerOpen ? (
|
|
295
|
+
<ModelPicker
|
|
296
|
+
config={config}
|
|
297
|
+
currentProvider={providerId}
|
|
298
|
+
currentModel={modelId}
|
|
299
|
+
onSelect={(newProvider, newModel) => {
|
|
300
|
+
engine.switchProvider(newProvider, newModel)
|
|
301
|
+
setProviderId(newProvider)
|
|
302
|
+
setModelId(newModel)
|
|
303
|
+
setPickerOpen(false)
|
|
304
|
+
setMessages((prev) => [
|
|
305
|
+
...prev,
|
|
306
|
+
{ role: 'system', content: `✓ Switched to ${newProvider}/${newModel}` },
|
|
307
|
+
])
|
|
308
|
+
}}
|
|
309
|
+
onClose={() => setPickerOpen(false)}
|
|
310
|
+
/>
|
|
311
|
+
) : (
|
|
312
|
+
/* Input bar (hidden when picker is open) */
|
|
313
|
+
<InputBar onSubmit={handleSubmit} isLoading={isLoading} />
|
|
314
|
+
)}
|
|
315
|
+
</Box>
|
|
316
|
+
)
|
|
317
|
+
}
|
package/src/ui/chat.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Box, Text } from 'ink'
|
|
3
|
+
import type { ChatMessage } from './app'
|
|
4
|
+
|
|
5
|
+
interface ChatPanelProps {
|
|
6
|
+
messages: ChatMessage[]
|
|
7
|
+
focusMode?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
|
|
11
|
+
// In focus mode, show only the last user+assistant exchange
|
|
12
|
+
const displayMessages = focusMode ? getLastExchange(messages) : messages
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<Box flexDirection="column" marginY={1} flexGrow={1}>
|
|
16
|
+
{focusMode && messages.length > 0 && (
|
|
17
|
+
<Box marginBottom={1}>
|
|
18
|
+
<Text dimColor>
|
|
19
|
+
🔍 Focus mode — showing last exchange only ({displayMessages.length} of{' '}
|
|
20
|
+
{messages.length} messages hidden)
|
|
21
|
+
</Text>
|
|
22
|
+
</Box>
|
|
23
|
+
)}
|
|
24
|
+
{messages.length === 0 && (
|
|
25
|
+
<Box flexDirection="column">
|
|
26
|
+
<Box marginBottom={1}>
|
|
27
|
+
<Text color="cyan" bold>
|
|
28
|
+
Mipham Code
|
|
29
|
+
</Text>
|
|
30
|
+
<Text dimColor> — AI-Powered Programming Assistant</Text>
|
|
31
|
+
</Box>
|
|
32
|
+
<Text dimColor>Multi-model · Multi-provider · Skills & Tools · Open-core</Text>
|
|
33
|
+
<Box marginTop={1}>
|
|
34
|
+
<Text dimColor>
|
|
35
|
+
Type a message to start. <Text color="yellow">/help</Text> for commands ·{' '}
|
|
36
|
+
<Text color="yellow">Ctrl+P</Text> pick model · <Text color="yellow">Esc</Text> to
|
|
37
|
+
exit
|
|
38
|
+
</Text>
|
|
39
|
+
</Box>
|
|
40
|
+
</Box>
|
|
41
|
+
)}
|
|
42
|
+
{displayMessages.map((msg, i) => (
|
|
43
|
+
<Box key={i} flexDirection="column" marginY={1}>
|
|
44
|
+
<Text
|
|
45
|
+
bold
|
|
46
|
+
color={msg.role === 'user' ? 'green' : msg.role === 'system' ? 'yellow' : 'blue'}
|
|
47
|
+
>
|
|
48
|
+
{msg.role === 'user' ? '▸ You' : msg.role === 'assistant' ? 'Mipham Code' : '⚠ System'}:
|
|
49
|
+
</Text>
|
|
50
|
+
<Text>{msg.content}</Text>
|
|
51
|
+
</Box>
|
|
52
|
+
))}
|
|
53
|
+
</Box>
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* In focus mode, show only the last user→assistant exchange.
|
|
59
|
+
* Walk backwards from the end to find the last user message, then include
|
|
60
|
+
* everything from that point onward.
|
|
61
|
+
*/
|
|
62
|
+
function getLastExchange(messages: ChatMessage[]): ChatMessage[] {
|
|
63
|
+
if (messages.length <= 2) return messages
|
|
64
|
+
|
|
65
|
+
// Find the last user message index
|
|
66
|
+
let lastUserIdx = -1
|
|
67
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
68
|
+
if (messages[i]!.role === 'user') {
|
|
69
|
+
lastUserIdx = i
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (lastUserIdx === -1) return messages
|
|
75
|
+
return messages.slice(lastUserIdx)
|
|
76
|
+
}
|