@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.
Files changed (67) hide show
  1. package/README.md +76 -0
  2. package/assets/icon.icns +0 -0
  3. package/assets/icon.jpg +0 -0
  4. package/bin/mipham +51 -0
  5. package/bin/mipham.ts +8 -0
  6. package/package.json +59 -0
  7. package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
  8. package/skills/mipham/om-security.mipham-skill.md +21 -0
  9. package/skills/standard/code-review.SKILL.md +116 -0
  10. package/skills/standard/compassionate-communication.SKILL.md +80 -0
  11. package/skills/standard/doc-generator.SKILL.md +21 -0
  12. package/skills/standard/github-ops.SKILL.md +22 -0
  13. package/skills/standard/memory.SKILL.md +22 -0
  14. package/skills/standard/mipham-code-setup.SKILL.md +113 -0
  15. package/skills/standard/security-review.SKILL.md +122 -0
  16. package/skills/standard/self-review.SKILL.md +20 -0
  17. package/skills/standard/superpower.SKILL.md +19 -0
  18. package/skills/standard/tdd.SKILL.md +20 -0
  19. package/skills/standard/web-search.SKILL.md +23 -0
  20. package/src/agent/sub-agent.ts +122 -0
  21. package/src/config/defaults.ts +10 -0
  22. package/src/config/loader.ts +30 -0
  23. package/src/core/context.ts +135 -0
  24. package/src/core/engine.ts +193 -0
  25. package/src/core/hooks.ts +116 -0
  26. package/src/core/instructions.ts +126 -0
  27. package/src/core/permission.ts +54 -0
  28. package/src/core/session-store.ts +136 -0
  29. package/src/index.tsx +93 -0
  30. package/src/mcp/client.ts +170 -0
  31. package/src/mcp/protocol.ts +104 -0
  32. package/src/mcp/transport.ts +169 -0
  33. package/src/mcp/types.ts +112 -0
  34. package/src/providers/anthropic.ts +286 -0
  35. package/src/providers/bootstrap.ts +28 -0
  36. package/src/providers/openai-compat.ts +158 -0
  37. package/src/providers/registry.ts +70 -0
  38. package/src/security/path.ts +90 -0
  39. package/src/security/url.ts +96 -0
  40. package/src/shared/constants.ts +368 -0
  41. package/src/shared/index.ts +2 -0
  42. package/src/shared/types.ts +161 -0
  43. package/src/skills/loader.ts +109 -0
  44. package/src/skills/mipham/runtime.ts +63 -0
  45. package/src/skills/standard/runtime.ts +59 -0
  46. package/src/tools/agent/agent.ts +51 -0
  47. package/src/tools/agent/memory.ts +51 -0
  48. package/src/tools/agent/plan.ts +87 -0
  49. package/src/tools/agent/skill.ts +58 -0
  50. package/src/tools/exec/bash.ts +111 -0
  51. package/src/tools/exec/git.ts +63 -0
  52. package/src/tools/exec/task.ts +69 -0
  53. package/src/tools/file/edit.ts +57 -0
  54. package/src/tools/file/glob.ts +29 -0
  55. package/src/tools/file/grep.ts +52 -0
  56. package/src/tools/file/read.ts +38 -0
  57. package/src/tools/file/write.ts +27 -0
  58. package/src/tools/index.ts +126 -0
  59. package/src/tools/network/web-fetch.ts +61 -0
  60. package/src/tools/network/web-search.ts +32 -0
  61. package/src/tools/system/config.ts +68 -0
  62. package/src/tools/system/mcp.ts +75 -0
  63. package/src/ui/app.tsx +317 -0
  64. package/src/ui/chat.tsx +76 -0
  65. package/src/ui/commands.ts +2232 -0
  66. package/src/ui/input.tsx +83 -0
  67. 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
+ }
@@ -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
+ }