@miphamai/cli 0.2.4 → 0.4.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/mipham.ts +2 -0
- package/dist/mipham +0 -0
- package/package.json +10 -9
- package/skills/mipham/om-artifact.mipham-skill.md +69 -0
- package/skills/mipham/om-model-optimize.mipham-skill.md +58 -9
- package/skills/mipham/om-security.mipham-skill.md +85 -10
- package/skills/standard/doc-generator.SKILL.md +72 -11
- package/skills/standard/github-ops.SKILL.md +94 -13
- package/skills/standard/memory.SKILL.md +72 -11
- package/skills/standard/self-review.SKILL.md +50 -10
- package/skills/standard/superpower.SKILL.md +46 -8
- package/skills/standard/tdd.SKILL.md +85 -12
- package/skills/standard/web-search.SKILL.md +65 -13
- package/src/agent/sub-agent.ts +0 -1
- package/src/artifacts/manifest.ts +101 -0
- package/src/artifacts/server.ts +343 -0
- package/src/core/context.ts +73 -13
- package/src/core/engine.ts +194 -71
- package/src/core/instructions.ts +1 -1
- package/src/core/permission.ts +35 -4
- package/src/core/session-store.ts +5 -1
- package/src/index.tsx +35 -2
- package/src/mcp/transport.ts +42 -5
- package/src/providers/anthropic.ts +2 -1
- package/src/providers/fetch-utils.ts +101 -0
- package/src/providers/openai-compat.ts +53 -15
- package/src/providers/registry.ts +1 -0
- package/src/shared/constants.ts +8 -1
- package/src/shared/types.ts +21 -1
- package/src/skills/loader.ts +2 -2
- package/src/tools/artifact/artifact.ts +144 -0
- package/src/tools/index.ts +3 -0
- package/src/ui/app.tsx +122 -15
- package/src/ui/chat.tsx +29 -7
- package/src/ui/commands.ts +117 -19
- package/src/ui/input.tsx +117 -35
- package/src/ui/picker.tsx +2 -5
package/src/ui/commands.ts
CHANGED
|
@@ -7,14 +7,8 @@
|
|
|
7
7
|
import type { QueryEngine } from '../core/engine'
|
|
8
8
|
import type { MiphamConfig } from '../shared/index.ts'
|
|
9
9
|
import type { SkillsLoader } from '../skills/loader'
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
NPM_INSTALL_COMMAND,
|
|
13
|
-
NPM_UPDATE_COMMAND,
|
|
14
|
-
NPM_URL,
|
|
15
|
-
PRODUCT_URL_INTERNATIONAL,
|
|
16
|
-
PRODUCT_URL_CHINA,
|
|
17
|
-
} from '@mipham/shared'
|
|
10
|
+
import { McpClient } from '../mcp/client'
|
|
11
|
+
import { NPM_INSTALL_COMMAND, NPM_UPDATE_COMMAND } from '@mipham/shared'
|
|
18
12
|
|
|
19
13
|
export interface CommandContext {
|
|
20
14
|
engine: QueryEngine
|
|
@@ -420,7 +414,6 @@ const renameCmd: CommandHandler = (ctx, args) => {
|
|
|
420
414
|
const goalCmd: CommandHandler = (ctx, args) => {
|
|
421
415
|
const goal = args.join(' ')
|
|
422
416
|
if (!goal.trim()) {
|
|
423
|
-
const c = ctx.engine.getContext()
|
|
424
417
|
return {
|
|
425
418
|
content: `Usage: /goal <statement>\n\nSet a session-level completion condition. Mipham Code will track progress toward this goal.\n\nExample: /goal Fix all TypeScript errors and make tests pass`,
|
|
426
419
|
}
|
|
@@ -866,7 +859,7 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
866
859
|
// ═══════════════════════════════════════════════════════════════
|
|
867
860
|
|
|
868
861
|
const exportCmd: CommandHandler = async (ctx) => {
|
|
869
|
-
const { writeFileSync
|
|
862
|
+
const { writeFileSync } = await import('node:fs')
|
|
870
863
|
const { join } = await import('node:path')
|
|
871
864
|
|
|
872
865
|
const cwd = process.cwd()
|
|
@@ -1868,18 +1861,40 @@ Works with: Bash, Zsh, Fish, PowerShell, Windows Terminal`,
|
|
|
1868
1861
|
// ═══════════════════════════════════════════════════════════════
|
|
1869
1862
|
|
|
1870
1863
|
const mcpCmd: CommandHandler = (ctx) => {
|
|
1871
|
-
const
|
|
1864
|
+
const configuredServers = ctx.config.skills?.mcpServers ?? []
|
|
1865
|
+
const liveConnections = McpClient.getInstance().listConnections()
|
|
1872
1866
|
|
|
1873
1867
|
const lines: string[] = ['── MCP Servers ──', '']
|
|
1874
1868
|
|
|
1875
|
-
if (
|
|
1876
|
-
lines.push(`Configured servers (${
|
|
1869
|
+
if (configuredServers.length > 0) {
|
|
1870
|
+
lines.push(`Configured servers (${configuredServers.length}):`)
|
|
1877
1871
|
lines.push('')
|
|
1878
|
-
for (const s of
|
|
1872
|
+
for (const s of configuredServers) {
|
|
1873
|
+
const live = liveConnections.find((c) => c.config.name === s.name)
|
|
1874
|
+
const statusIcon = live
|
|
1875
|
+
? live.status === 'connected'
|
|
1876
|
+
? '🟢'
|
|
1877
|
+
: live.status === 'connecting'
|
|
1878
|
+
? '🟡'
|
|
1879
|
+
: live.status === 'error'
|
|
1880
|
+
? '🔴'
|
|
1881
|
+
: '⚪'
|
|
1882
|
+
: '⚪'
|
|
1883
|
+
const statusLabel = live ? live.status : 'not started'
|
|
1879
1884
|
const envKeys = s.env ? Object.keys(s.env).join(', ') : '(none)'
|
|
1880
|
-
|
|
1885
|
+
|
|
1886
|
+
lines.push(` ${statusIcon} ${s.name} [${statusLabel}]`)
|
|
1881
1887
|
lines.push(` Command: ${s.command} ${s.args.join(' ')}`)
|
|
1882
1888
|
lines.push(` Env vars: ${envKeys}`)
|
|
1889
|
+
if (live?.tools && live.tools.length > 0) {
|
|
1890
|
+
lines.push(` Tools: ${live.tools.map((t) => t.name).join(', ')}`)
|
|
1891
|
+
}
|
|
1892
|
+
if (live?.error) {
|
|
1893
|
+
lines.push(` Error: ${live.error}`)
|
|
1894
|
+
}
|
|
1895
|
+
if (live?.serverInfo) {
|
|
1896
|
+
lines.push(` Server: ${live.serverInfo.name} v${live.serverInfo.version}`)
|
|
1897
|
+
}
|
|
1883
1898
|
lines.push('')
|
|
1884
1899
|
}
|
|
1885
1900
|
} else {
|
|
@@ -1908,11 +1923,10 @@ const mcpCmd: CommandHandler = (ctx) => {
|
|
|
1908
1923
|
}
|
|
1909
1924
|
|
|
1910
1925
|
lines.push('')
|
|
1911
|
-
lines.push('──
|
|
1912
|
-
lines.push('MCP stdio
|
|
1913
|
-
lines.push('
|
|
1926
|
+
lines.push('── Protocol ──')
|
|
1927
|
+
lines.push('MCP stdio transport (JSON-RPC 2.0) — fully implemented.')
|
|
1928
|
+
lines.push('Servers auto-connect on startup when configured.')
|
|
1914
1929
|
lines.push('')
|
|
1915
|
-
lines.push('MCP enables AI to interact with external tools via standardized servers.')
|
|
1916
1930
|
lines.push('Learn more: https://modelcontextprotocol.io')
|
|
1917
1931
|
|
|
1918
1932
|
return { content: lines.join('\n') }
|
|
@@ -2106,6 +2120,87 @@ Mipham Code includes 4 agent-type tools for structured workflows:
|
|
|
2106
2120
|
Use /tools to see all 16 tools, including the 4 agent tools.`,
|
|
2107
2121
|
})
|
|
2108
2122
|
|
|
2123
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2124
|
+
// Artifacts
|
|
2125
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2126
|
+
|
|
2127
|
+
async function openBrowser(url: string): Promise<void> {
|
|
2128
|
+
const cmd =
|
|
2129
|
+
process.platform === 'darwin'
|
|
2130
|
+
? `open "${url}"`
|
|
2131
|
+
: process.platform === 'win32'
|
|
2132
|
+
? `start "" "${url}"`
|
|
2133
|
+
: `xdg-open "${url}"`
|
|
2134
|
+
const { exec } = await import('node:child_process')
|
|
2135
|
+
exec(cmd, () => {
|
|
2136
|
+
/* fire-and-forget */
|
|
2137
|
+
})
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
const artifactOpenCmd: CommandHandler = async (ctx, args) => {
|
|
2141
|
+
const name = args[0]
|
|
2142
|
+
if (!name) {
|
|
2143
|
+
return { content: 'Usage: /artifact open <name>\nExample: /artifact open dashboard' }
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
const sessionId = 'session-1' // default session
|
|
2147
|
+
const port = 9876
|
|
2148
|
+
const ext = name.endsWith('.svg') ? '' : '.html'
|
|
2149
|
+
const url = `http://localhost:${port}/${sessionId}/${name}${ext}`
|
|
2150
|
+
|
|
2151
|
+
try {
|
|
2152
|
+
await openBrowser(url)
|
|
2153
|
+
return { content: `✓ Opening artifact "${name}" in browser...\n ${url}` }
|
|
2154
|
+
} catch (err) {
|
|
2155
|
+
return { content: `✗ Failed to open browser: ${String(err)}\n URL: ${url}` }
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
const artifactListCmd: CommandHandler = async (_ctx, _args) => {
|
|
2160
|
+
const { getSessionArtifacts } = await import('../artifacts/manifest')
|
|
2161
|
+
const { join } = await import('node:path')
|
|
2162
|
+
const { ARTIFACTS_DIR } = await import('../shared/constants')
|
|
2163
|
+
|
|
2164
|
+
const dir = join(process.cwd(), ARTIFACTS_DIR)
|
|
2165
|
+
const entries = getSessionArtifacts(dir, 'session-1')
|
|
2166
|
+
|
|
2167
|
+
if (entries.length === 0) {
|
|
2168
|
+
return {
|
|
2169
|
+
content: 'No artifacts created yet. Ask the AI to generate one with the Artifact tool.',
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
const lines = ['── Artifacts ──', '']
|
|
2174
|
+
for (const e of entries) {
|
|
2175
|
+
const ver = e.versions && e.versions.length > 1 ? ` v${e.versions.length}` : ''
|
|
2176
|
+
lines.push(
|
|
2177
|
+
` ${e.name.padEnd(24)} ${(e.type + ver).padEnd(8)} ${(e.size / 1024).toFixed(1).padStart(8)}KB ${e.createdAt.slice(0, 16).replace('T', ' ')}`,
|
|
2178
|
+
)
|
|
2179
|
+
}
|
|
2180
|
+
lines.push('', ` ${entries.length} artifact(s) — /artifact open <name> to view`)
|
|
2181
|
+
lines.push(` Gallery: http://localhost:9876`)
|
|
2182
|
+
|
|
2183
|
+
return { content: lines.join('\n') }
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
const artifactServerCmd: CommandHandler = async (_ctx, _args) => {
|
|
2187
|
+
const url = 'http://localhost:9876'
|
|
2188
|
+
return {
|
|
2189
|
+
content: `── Artifact Server ──\n\n URL: ${url}\n Port: 9876\n\nArtifacts are served locally. No external network access.`,
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
const artifactBaseCmd: CommandHandler = (ctx, args) => {
|
|
2194
|
+
const sub = args[0]
|
|
2195
|
+
if (sub === 'open') return artifactOpenCmd(ctx, args.slice(1))
|
|
2196
|
+
if (sub === 'list') return artifactListCmd(ctx, [])
|
|
2197
|
+
if (sub === 'server') return artifactServerCmd(ctx, [])
|
|
2198
|
+
return {
|
|
2199
|
+
content:
|
|
2200
|
+
'Usage: /artifact <open|list|server> [name]\n\n /artifact open <name> Open artifact in browser\n /artifact list List all artifacts\n /artifact server Show server status',
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2109
2204
|
// ═══════════════════════════════════════════════════════════════
|
|
2110
2205
|
// Remaining low-priority stubs (backend not yet available)
|
|
2111
2206
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -2192,6 +2287,9 @@ registry.set('/logout', logoutCmd)
|
|
|
2192
2287
|
registry.set('/feedback', feedbackCmd)
|
|
2193
2288
|
registry.set('/agents', agentsCmd)
|
|
2194
2289
|
|
|
2290
|
+
// Artifacts
|
|
2291
|
+
registry.set('/artifact', artifactBaseCmd)
|
|
2292
|
+
|
|
2195
2293
|
// Lower-priority semi-stubs (WIP, backend pending)
|
|
2196
2294
|
registry.set('/branch', branchCmd)
|
|
2197
2295
|
registry.set('/schedule', scheduleCmd)
|
package/src/ui/input.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react'
|
|
1
|
+
import React, { useState, useEffect, useRef } from 'react'
|
|
2
2
|
import { Box, Text } from 'ink'
|
|
3
3
|
import TextInput from 'ink-text-input'
|
|
4
4
|
|
|
@@ -7,32 +7,98 @@ interface InputBarProps {
|
|
|
7
7
|
isLoading: boolean
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
// ──
|
|
10
|
+
// ── Status verbs — English (Claude Code-aligned) + Chinese (Mipham originals) ──
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
12
|
+
const STATUS_EN = [
|
|
13
|
+
'Doodling',
|
|
14
|
+
'Forging',
|
|
15
|
+
'Germinating',
|
|
16
|
+
'Mosmosing',
|
|
17
|
+
'Cerebrating',
|
|
18
|
+
'Boogieing',
|
|
19
|
+
'Finagling',
|
|
20
|
+
'Misting',
|
|
21
|
+
'Recombobulating',
|
|
22
|
+
'Slithering',
|
|
23
|
+
'Effecting',
|
|
24
|
+
'Roosting',
|
|
25
|
+
'Crystallizing',
|
|
26
|
+
'Canoodling',
|
|
27
|
+
'Billowing',
|
|
28
|
+
'Warping',
|
|
29
|
+
'Improvising',
|
|
30
|
+
'Metamorphing',
|
|
31
|
+
'Gusting',
|
|
32
|
+
'Whiring',
|
|
33
|
+
'Topsy-turvying',
|
|
34
|
+
'Flibbertigibetting',
|
|
35
|
+
'Wibbling',
|
|
36
|
+
'Thundering',
|
|
37
|
+
'Moseying',
|
|
38
|
+
'Julienning',
|
|
39
|
+
'Evaporating',
|
|
40
|
+
'Ruminating',
|
|
41
|
+
'Whisking',
|
|
42
|
+
'Scampering',
|
|
43
|
+
'Meandering',
|
|
44
|
+
'Concoting',
|
|
45
|
+
'Gesticulating',
|
|
46
|
+
'Flamebeing',
|
|
47
|
+
'Quantumizing',
|
|
48
|
+
'Waddling',
|
|
49
|
+
'Fluttering',
|
|
50
|
+
'Sprouting',
|
|
51
|
+
'Elucidating',
|
|
52
|
+
'Embellishing',
|
|
53
|
+
'Razzmatizing',
|
|
54
|
+
'Pondering',
|
|
55
|
+
'Cogitating',
|
|
56
|
+
'Garnishing',
|
|
57
|
+
'Actualizing',
|
|
58
|
+
'Zigzagging',
|
|
59
|
+
'Mustering',
|
|
60
|
+
'Frolicking',
|
|
61
|
+
'Hullaballooing',
|
|
62
|
+
'Doing',
|
|
63
|
+
'Fermenting',
|
|
64
|
+
'Considering',
|
|
65
|
+
'Skedaddling',
|
|
66
|
+
'Actioning',
|
|
67
|
+
'Orbiting',
|
|
68
|
+
'Perambulating',
|
|
69
|
+
'Drizzling',
|
|
70
|
+
'Schlepping',
|
|
71
|
+
'Ionizing',
|
|
72
|
+
'Scurrying',
|
|
33
73
|
]
|
|
34
74
|
|
|
35
|
-
const
|
|
75
|
+
const STATUS_CN = [
|
|
76
|
+
'思忖中',
|
|
77
|
+
'推演中',
|
|
78
|
+
'凝炼中',
|
|
79
|
+
'熬煮中',
|
|
80
|
+
'锻造中',
|
|
81
|
+
'研磨中',
|
|
82
|
+
'解构中',
|
|
83
|
+
'冥思中',
|
|
84
|
+
'编织中',
|
|
85
|
+
'萃取中',
|
|
86
|
+
'烹制中',
|
|
87
|
+
'淬火中',
|
|
88
|
+
'雕琢中',
|
|
89
|
+
'融汇中',
|
|
90
|
+
'觉照中',
|
|
91
|
+
'运转中',
|
|
92
|
+
'参详中',
|
|
93
|
+
'化合中',
|
|
94
|
+
'浸润中',
|
|
95
|
+
'焙烤中',
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
// Merge both — bilingual status verbs
|
|
99
|
+
const STATUS_GERUNDS = [...STATUS_EN, ...STATUS_CN]
|
|
100
|
+
|
|
101
|
+
const STATUS_PAST = ['Brewed', 'Churned', 'Cooked', 'Sautéed', 'Cogitated', 'Crunched']
|
|
36
102
|
|
|
37
103
|
function pick<T>(arr: T[]): T {
|
|
38
104
|
return arr[Math.floor(Math.random() * arr.length)]!
|
|
@@ -40,25 +106,35 @@ function pick<T>(arr: T[]): T {
|
|
|
40
106
|
|
|
41
107
|
export function InputBar({ onSubmit, isLoading }: InputBarProps) {
|
|
42
108
|
const [value, setValue] = useState('')
|
|
43
|
-
const [verb, setVerb] = useState(() => pick(
|
|
44
|
-
const [
|
|
109
|
+
const [verb, setVerb] = useState(() => pick(STATUS_GERUNDS))
|
|
110
|
+
const [completionVerb, setCompletionVerb] = useState<string | null>(null)
|
|
111
|
+
const prevLoading = useRef(isLoading)
|
|
45
112
|
|
|
46
|
-
// Rotate
|
|
113
|
+
// Rotate gerunds while loading
|
|
47
114
|
useEffect(() => {
|
|
48
115
|
if (!isLoading) return
|
|
49
116
|
const interval = setInterval(() => {
|
|
50
|
-
setVerb(pick(
|
|
51
|
-
|
|
52
|
-
}, 2000)
|
|
117
|
+
setVerb(pick(STATUS_GERUNDS))
|
|
118
|
+
}, 1200)
|
|
53
119
|
return () => clearInterval(interval)
|
|
54
120
|
}, [isLoading])
|
|
55
121
|
|
|
56
|
-
//
|
|
122
|
+
// Pick a fresh gerund when loading starts
|
|
57
123
|
useEffect(() => {
|
|
58
124
|
if (isLoading) {
|
|
59
|
-
setVerb(pick(
|
|
60
|
-
|
|
125
|
+
setVerb(pick(STATUS_GERUNDS))
|
|
126
|
+
setCompletionVerb(null)
|
|
127
|
+
}
|
|
128
|
+
}, [isLoading])
|
|
129
|
+
|
|
130
|
+
// Flash a past participle when loading stops
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (prevLoading.current === true && isLoading === false) {
|
|
133
|
+
setCompletionVerb(pick(STATUS_PAST))
|
|
134
|
+
const timer = setTimeout(() => setCompletionVerb(null), 1500)
|
|
135
|
+
return () => clearTimeout(timer)
|
|
61
136
|
}
|
|
137
|
+
prevLoading.current = isLoading
|
|
62
138
|
}, [isLoading])
|
|
63
139
|
|
|
64
140
|
const handleSubmit = (val: string) => {
|
|
@@ -70,13 +146,19 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
|
|
|
70
146
|
return (
|
|
71
147
|
<Box marginTop={1}>
|
|
72
148
|
<Box marginRight={1}>
|
|
73
|
-
<Text color={isLoading ? 'yellow' : 'cyan'}>{
|
|
149
|
+
<Text color={isLoading ? 'yellow' : 'cyan'}>{'>'}</Text>
|
|
74
150
|
</Box>
|
|
75
151
|
<TextInput
|
|
76
152
|
value={value}
|
|
77
153
|
onChange={setValue}
|
|
78
154
|
onSubmit={handleSubmit}
|
|
79
|
-
placeholder={
|
|
155
|
+
placeholder={
|
|
156
|
+
isLoading
|
|
157
|
+
? `${verb}...`
|
|
158
|
+
: completionVerb
|
|
159
|
+
? completionVerb
|
|
160
|
+
: 'Type a message (Esc to cancel)...'
|
|
161
|
+
}
|
|
80
162
|
/>
|
|
81
163
|
</Box>
|
|
82
164
|
)
|
package/src/ui/picker.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useState, useCallback
|
|
1
|
+
import React, { useState, useCallback } from 'react'
|
|
2
2
|
import { Box, Text, useInput } from 'ink'
|
|
3
|
-
import type { MiphamConfig, ProviderConfig
|
|
3
|
+
import type { MiphamConfig, ProviderConfig } from '../shared/index.ts'
|
|
4
4
|
|
|
5
5
|
interface PickerProps {
|
|
6
6
|
config: MiphamConfig
|
|
@@ -113,9 +113,6 @@ export function ModelPicker({
|
|
|
113
113
|
// Provider labels
|
|
114
114
|
const providerColor = (p: ProviderConfig) => (p.id === currentProvider ? 'green' : 'white')
|
|
115
115
|
|
|
116
|
-
const modelColor = (m: ModelInfo) =>
|
|
117
|
-
selectedProvider?.id === currentProvider && m.id === currentModel ? 'green' : 'white'
|
|
118
|
-
|
|
119
116
|
return (
|
|
120
117
|
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
|
|
121
118
|
{/* Title */}
|