@miphamai/cli 0.16.1 → 0.16.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/bin/mipham.ts +61 -0
- package/package.json +2 -1
- package/src/ui/app.tsx +61 -61
- package/src/ui/input.tsx +61 -2
- package/dist/mipham +0 -0
package/bin/mipham.ts
CHANGED
|
@@ -331,6 +331,32 @@ async function main() {
|
|
|
331
331
|
process.exit(0)
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
// ── Help flag ─────────────────────────────────────────────────────────────
|
|
335
|
+
if (
|
|
336
|
+
process.argv.includes('--help') ||
|
|
337
|
+
process.argv.includes('-h') ||
|
|
338
|
+
process.argv.slice(2).some((a) => a === 'help')
|
|
339
|
+
) {
|
|
340
|
+
console.log(`Mipham Code — AI-powered coding terminal
|
|
341
|
+
|
|
342
|
+
Usage:
|
|
343
|
+
mipham Launch interactive CLI
|
|
344
|
+
mipham update Update to the latest version
|
|
345
|
+
mipham upgrade Same as 'mipham update'
|
|
346
|
+
mipham plugin <cmd> Plugin management (install, list, remove, etc.)
|
|
347
|
+
mipham workflow <cmd> Workflow orchestration (run, list, resume, etc.)
|
|
348
|
+
mipham --version Print version and exit
|
|
349
|
+
mipham --help Show this help
|
|
350
|
+
|
|
351
|
+
Flags:
|
|
352
|
+
--safe-mode Skip custom agents, skills, hooks, plugins
|
|
353
|
+
--version, -v, -V Print version
|
|
354
|
+
|
|
355
|
+
Docs: https://mipham.ai/code
|
|
356
|
+
npm: https://www.npmjs.com/package/@miphamai/cli`)
|
|
357
|
+
process.exit(0)
|
|
358
|
+
}
|
|
359
|
+
|
|
334
360
|
// ── Update / upgrade ──────────────────────────────────────────────────────
|
|
335
361
|
const handledUpdate = await runUpdate()
|
|
336
362
|
if (handledUpdate) return
|
|
@@ -343,6 +369,41 @@ async function main() {
|
|
|
343
369
|
const handled = await runWorkflowCLI()
|
|
344
370
|
if (handled) return
|
|
345
371
|
|
|
372
|
+
// ── Unknown command detection ──────────────────────────────────────────────
|
|
373
|
+
// After all known subcommands are checked, any remaining positional argument
|
|
374
|
+
// is probably a typo. Show error + suggestions instead of silently launching CLI.
|
|
375
|
+
const KNOWN_COMMANDS = ['update', 'upgrade', 'plugin', 'workflow', 'help']
|
|
376
|
+
const firstArg = process.argv.slice(2).find((a) => !a.startsWith('-'))
|
|
377
|
+
if (firstArg) {
|
|
378
|
+
// Levenshtein distance to find closest match
|
|
379
|
+
const distance = (a: string, b: string): number => {
|
|
380
|
+
const m = a.length
|
|
381
|
+
const n = b.length
|
|
382
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
|
|
383
|
+
for (let i = 0; i <= m; i++) dp[i]![0] = i
|
|
384
|
+
for (let j = 0; j <= n; j++) dp[0]![j] = j
|
|
385
|
+
for (let i = 1; i <= m; i++)
|
|
386
|
+
for (let j = 1; j <= n; j++)
|
|
387
|
+
dp[i]![j] =
|
|
388
|
+
a[i - 1] === b[j - 1]
|
|
389
|
+
? dp[i - 1]![j - 1]!
|
|
390
|
+
: 1 + Math.min(dp[i - 1]![j]!, dp[i]![j - 1]!, dp[i - 1]![j - 1]!)
|
|
391
|
+
return dp[m]![n]!
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const best = KNOWN_COMMANDS.map((cmd) => ({ cmd, dist: distance(firstArg, cmd) }))
|
|
395
|
+
.filter((c) => c.dist <= 3)
|
|
396
|
+
.sort((a, b) => a.dist - b.dist)
|
|
397
|
+
const suggestions = best.map((c) => `'mipham ${c.cmd}'`).join(', ')
|
|
398
|
+
|
|
399
|
+
console.error(`Unknown command: mipham ${firstArg}`)
|
|
400
|
+
if (suggestions) {
|
|
401
|
+
console.error(`Did you mean: ${suggestions}?`)
|
|
402
|
+
}
|
|
403
|
+
console.error(`Run 'mipham --help' for usage.`)
|
|
404
|
+
process.exit(1)
|
|
405
|
+
}
|
|
406
|
+
|
|
346
407
|
// Parse --safe-mode flag: skip custom agents, skills, hooks, plugins
|
|
347
408
|
const safeModeFlag = process.argv.includes('--safe-mode')
|
|
348
409
|
if (safeModeFlag) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miphamai/cli",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.3",
|
|
4
4
|
"description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"test": "vitest run"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
+
"commander": "^13.1.0",
|
|
45
46
|
"ink": "^5.2.1",
|
|
46
47
|
"ink-text-input": "^6.0.0",
|
|
47
48
|
"react": "^18.3.1",
|
package/src/ui/app.tsx
CHANGED
|
@@ -68,12 +68,12 @@ const PERMISSION_MODES: PermissionMode[] = [
|
|
|
68
68
|
'bypassPermissions',
|
|
69
69
|
]
|
|
70
70
|
const PERMISSION_LABELS: Record<PermissionMode, string> = {
|
|
71
|
-
default: '
|
|
72
|
-
acceptEdits: 'accept edits
|
|
71
|
+
default: 'reads free',
|
|
72
|
+
acceptEdits: 'accept edits on',
|
|
73
73
|
plan: 'plan mode · read-only',
|
|
74
|
-
auto: 'auto mode
|
|
75
|
-
dontAsk: "don't ask
|
|
76
|
-
bypassPermissions: 'bypass
|
|
74
|
+
auto: 'auto mode',
|
|
75
|
+
dontAsk: "don't ask",
|
|
76
|
+
bypassPermissions: 'bypass',
|
|
77
77
|
}
|
|
78
78
|
const PERMISSION_COLORS: Record<PermissionMode, string> = {
|
|
79
79
|
default: 'white',
|
|
@@ -500,7 +500,8 @@ export function App({
|
|
|
500
500
|
)
|
|
501
501
|
|
|
502
502
|
useInput((_input, key) => {
|
|
503
|
-
//
|
|
503
|
+
// Escape: close picker → abort loading (does NOT exit app)
|
|
504
|
+
// Vim mode toggle is handled by InputBar
|
|
504
505
|
if (key.escape) {
|
|
505
506
|
if (pickerOpen) {
|
|
506
507
|
setPickerOpen(false)
|
|
@@ -510,57 +511,12 @@ export function App({
|
|
|
510
511
|
abortRef.current.abort()
|
|
511
512
|
return
|
|
512
513
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
// Ctrl+P → open model picker
|
|
516
|
-
if (_input === '\x10') {
|
|
517
|
-
setPickerOpen((prev) => !prev)
|
|
518
|
-
return
|
|
519
|
-
}
|
|
520
|
-
// Ctrl+F → toggle focus mode
|
|
521
|
-
if (_input === '\x06') {
|
|
522
|
-
setFocusMode((prev) => !prev)
|
|
523
|
-
return
|
|
524
|
-
}
|
|
525
|
-
// Ctrl+O → toggle last tool call expand/collapse
|
|
526
|
-
if (_input === '\x0f') {
|
|
527
|
-
setMessages((prev) => {
|
|
528
|
-
const msgs = [...prev]
|
|
529
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
530
|
-
if (msgs[i]?.toolMeta) {
|
|
531
|
-
const meta = msgs[i]!.toolMeta!
|
|
532
|
-
if (meta.collapsed) {
|
|
533
|
-
msgs[i] = {
|
|
534
|
-
...msgs[i]!,
|
|
535
|
-
content: `🔧 ${meta.name}: ${meta.input}\n📋 Result: ${meta.output || '(pending)'}`,
|
|
536
|
-
toolMeta: { ...meta, collapsed: false },
|
|
537
|
-
}
|
|
538
|
-
} else {
|
|
539
|
-
const short = meta.input.length > 50 ? meta.input.slice(0, 50) + '...' : meta.input
|
|
540
|
-
msgs[i] = {
|
|
541
|
-
...msgs[i]!,
|
|
542
|
-
content: `⏺ ${meta.name} · ${short} (Ctrl+O to expand)`,
|
|
543
|
-
toolMeta: { ...meta, collapsed: true },
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
break
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
return msgs
|
|
550
|
-
})
|
|
551
|
-
return
|
|
552
|
-
}
|
|
553
|
-
// Shift+Tab → cycle through all 6 permission modes
|
|
554
|
-
if (key.shift && key.tab) {
|
|
555
|
-
setPermissionMode((prev) => {
|
|
556
|
-
const idx = PERMISSION_MODES.indexOf(prev)
|
|
557
|
-
const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
|
|
558
|
-
// Sync to engine — use setMode for proper mode semantics
|
|
559
|
-
engine.getPermission().setMode(next)
|
|
560
|
-
return next
|
|
561
|
-
})
|
|
514
|
+
// When input is empty and not loading, let InputBar handle Escape
|
|
515
|
+
// (it toggles vim mode or clears pending sequences)
|
|
562
516
|
return
|
|
563
517
|
}
|
|
518
|
+
// All other global hotkeys (Shift+Tab, Ctrl+P, Ctrl+F, Ctrl+O)
|
|
519
|
+
// are handled in InputBar to avoid ink-text-input conflicts
|
|
564
520
|
})
|
|
565
521
|
|
|
566
522
|
return (
|
|
@@ -599,7 +555,52 @@ export function App({
|
|
|
599
555
|
/* Input bar (hidden when picker is open) */
|
|
600
556
|
<Box flexDirection="column">
|
|
601
557
|
<Text dimColor>──────────────────────────────</Text>
|
|
602
|
-
<InputBar
|
|
558
|
+
<InputBar
|
|
559
|
+
onSubmit={handleSubmit}
|
|
560
|
+
isLoading={isLoading}
|
|
561
|
+
onTogglePicker={() => setPickerOpen((prev) => !prev)}
|
|
562
|
+
onToggleFocus={() => setFocusMode((prev) => !prev)}
|
|
563
|
+
onToggleExpand={() => {
|
|
564
|
+
setMessages((prev) => {
|
|
565
|
+
const msgs = [...prev]
|
|
566
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
567
|
+
if (msgs[i]?.toolMeta) {
|
|
568
|
+
const meta = msgs[i]!.toolMeta!
|
|
569
|
+
if (meta.collapsed) {
|
|
570
|
+
msgs[i] = {
|
|
571
|
+
...msgs[i]!,
|
|
572
|
+
content: `🔧 ${meta.name}: ${meta.input}\n📋 Result: ${meta.output || '(pending)'}`,
|
|
573
|
+
toolMeta: { ...meta, collapsed: false },
|
|
574
|
+
}
|
|
575
|
+
} else {
|
|
576
|
+
const short =
|
|
577
|
+
meta.input.length > 50 ? meta.input.slice(0, 50) + '...' : meta.input
|
|
578
|
+
msgs[i] = {
|
|
579
|
+
...msgs[i]!,
|
|
580
|
+
content: `⏺ ${meta.name} · ${short} (Ctrl+O to expand)`,
|
|
581
|
+
toolMeta: { ...meta, collapsed: true },
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
break
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return msgs
|
|
588
|
+
})
|
|
589
|
+
}}
|
|
590
|
+
onCyclePermission={() => {
|
|
591
|
+
setPermissionMode((prev) => {
|
|
592
|
+
const idx = PERMISSION_MODES.indexOf(prev)
|
|
593
|
+
const next = PERMISSION_MODES[(idx + 1) % PERMISSION_MODES.length]!
|
|
594
|
+
engine.getPermission().setMode(next)
|
|
595
|
+
return next
|
|
596
|
+
})
|
|
597
|
+
}}
|
|
598
|
+
onCancel={() => {
|
|
599
|
+
if (abortRef.current) {
|
|
600
|
+
abortRef.current.abort()
|
|
601
|
+
}
|
|
602
|
+
}}
|
|
603
|
+
/>
|
|
603
604
|
<Text dimColor>──────────────────────────────</Text>
|
|
604
605
|
</Box>
|
|
605
606
|
)}
|
|
@@ -607,7 +608,7 @@ export function App({
|
|
|
607
608
|
{/* Agent status footer — shows running background agents */}
|
|
608
609
|
<AgentFooter agents={Object.values(runningAgents)} gitBranch={gitBranch} tick={agentTick} />
|
|
609
610
|
|
|
610
|
-
{/* Status line */}
|
|
611
|
+
{/* Status line — Claude Code style: compact, action-oriented */}
|
|
611
612
|
<Box marginTop={1} flexDirection="column">
|
|
612
613
|
{goalText && (
|
|
613
614
|
<Box>
|
|
@@ -616,11 +617,10 @@ export function App({
|
|
|
616
617
|
)}
|
|
617
618
|
<Box flexDirection="row">
|
|
618
619
|
<Text color={PERMISSION_COLORS[permissionMode]}>
|
|
619
|
-
|
|
620
|
+
⏵⏵ {PERMISSION_LABELS[permissionMode]} (shift+tab to cycle)
|
|
620
621
|
</Text>
|
|
621
|
-
<Text dimColor> ·
|
|
622
|
-
<Text dimColor> ·
|
|
623
|
-
<Text dimColor> · Esc cancel · /help · ← agents</Text>
|
|
622
|
+
<Text dimColor> · Esc to interrupt</Text>
|
|
623
|
+
<Text dimColor> · ← agents</Text>
|
|
624
624
|
</Box>
|
|
625
625
|
</Box>
|
|
626
626
|
</Box>
|
package/src/ui/input.tsx
CHANGED
|
@@ -8,6 +8,16 @@ import { CommandPicker } from './command-picker.js'
|
|
|
8
8
|
interface InputBarProps {
|
|
9
9
|
onSubmit: (input: string) => void
|
|
10
10
|
isLoading: boolean
|
|
11
|
+
/** Ctrl+P → open model picker */
|
|
12
|
+
onTogglePicker?: () => void
|
|
13
|
+
/** Ctrl+F → toggle focus mode */
|
|
14
|
+
onToggleFocus?: () => void
|
|
15
|
+
/** Ctrl+O → expand last tool call */
|
|
16
|
+
onToggleExpand?: () => void
|
|
17
|
+
/** Shift+Tab → cycle permission mode */
|
|
18
|
+
onCyclePermission?: () => void
|
|
19
|
+
/** Escape → cancel loading (when input is empty) */
|
|
20
|
+
onCancel?: () => void
|
|
11
21
|
}
|
|
12
22
|
|
|
13
23
|
// ── Status verbs — English (Claude Code-aligned) + Chinese (Mipham originals) ──
|
|
@@ -107,7 +117,15 @@ function pick<T>(arr: T[]): T {
|
|
|
107
117
|
return arr[Math.floor(Math.random() * arr.length)]!
|
|
108
118
|
}
|
|
109
119
|
|
|
110
|
-
export function InputBar({
|
|
120
|
+
export function InputBar({
|
|
121
|
+
onSubmit,
|
|
122
|
+
isLoading,
|
|
123
|
+
onTogglePicker,
|
|
124
|
+
onToggleFocus,
|
|
125
|
+
onToggleExpand,
|
|
126
|
+
onCyclePermission,
|
|
127
|
+
onCancel,
|
|
128
|
+
}: InputBarProps) {
|
|
111
129
|
const [value, setValue] = useState('')
|
|
112
130
|
const [verb, setVerb] = useState(() => pick(STATUS_GERUNDS))
|
|
113
131
|
const [completionVerb, setCompletionVerb] = useState<string | null>(null)
|
|
@@ -158,10 +176,19 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
|
|
|
158
176
|
const [searchMode, setSearchMode] = useState(false)
|
|
159
177
|
const [searchQuery, setSearchQuery] = useState('')
|
|
160
178
|
|
|
179
|
+
// Track pre-shortcut value so we can revert ink-text-input's Ctrl-key insertions.
|
|
180
|
+
// ink-text-input inserts 'p'/'f'/'o' for Ctrl+P/F/O (Ink normalizes control chars
|
|
181
|
+
// to their letter names before passing to useInput).
|
|
182
|
+
const valueBeforeShortcut = useRef(value)
|
|
183
|
+
// Keep the ref in sync with state on every change that isn't a Ctrl shortcut revert.
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
valueBeforeShortcut.current = value
|
|
186
|
+
}, [value])
|
|
187
|
+
|
|
161
188
|
// ── Vim motions: intercept keys in normal mode ──
|
|
162
189
|
|
|
163
190
|
useInput((input, key) => {
|
|
164
|
-
// Escape
|
|
191
|
+
// ── Escape: cancel loading → toggle vim mode ──
|
|
165
192
|
if (key.escape) {
|
|
166
193
|
// Cancel search mode if active
|
|
167
194
|
if (searchMode) {
|
|
@@ -169,6 +196,11 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
|
|
|
169
196
|
setSearchQuery('')
|
|
170
197
|
return
|
|
171
198
|
}
|
|
199
|
+
// Escape while loading → abort (priority over vim toggle)
|
|
200
|
+
if (isLoading) {
|
|
201
|
+
onCancel?.()
|
|
202
|
+
return
|
|
203
|
+
}
|
|
172
204
|
// Clear any pending multi-key sequence
|
|
173
205
|
if (vimPending.current) {
|
|
174
206
|
vimPending.current = null
|
|
@@ -178,6 +210,33 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
|
|
|
178
210
|
return
|
|
179
211
|
}
|
|
180
212
|
|
|
213
|
+
// ── Global hotkeys (work in both insert and normal modes) ──
|
|
214
|
+
// Shift+Tab → cycle permission mode
|
|
215
|
+
if (key.shift && key.tab) {
|
|
216
|
+
onCyclePermission?.()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
// Ctrl+P → toggle model picker
|
|
220
|
+
// NOTE: Ink passes input=keypress.name (just 'p') when ctrl is true, not raw \x10.
|
|
221
|
+
// ink-text-input inserts 'p' as literal text — revert it.
|
|
222
|
+
if (key.ctrl && input === 'p') {
|
|
223
|
+
setValue(valueBeforeShortcut.current)
|
|
224
|
+
onTogglePicker?.()
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
// Ctrl+F → toggle focus mode
|
|
228
|
+
if (key.ctrl && input === 'f') {
|
|
229
|
+
setValue(valueBeforeShortcut.current)
|
|
230
|
+
onToggleFocus?.()
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
// Ctrl+O → expand/collapse last tool call
|
|
234
|
+
if (key.ctrl && input === 'o') {
|
|
235
|
+
setValue(valueBeforeShortcut.current)
|
|
236
|
+
onToggleExpand?.()
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
|
|
181
240
|
if (vimEngine.current.mode !== 'normal') return
|
|
182
241
|
|
|
183
242
|
// Handle search mode — collect query characters
|
package/dist/mipham
DELETED
|
Binary file
|