@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.
Files changed (37) hide show
  1. package/bin/mipham.ts +2 -0
  2. package/dist/mipham +0 -0
  3. package/package.json +10 -9
  4. package/skills/mipham/om-artifact.mipham-skill.md +69 -0
  5. package/skills/mipham/om-model-optimize.mipham-skill.md +58 -9
  6. package/skills/mipham/om-security.mipham-skill.md +85 -10
  7. package/skills/standard/doc-generator.SKILL.md +72 -11
  8. package/skills/standard/github-ops.SKILL.md +94 -13
  9. package/skills/standard/memory.SKILL.md +72 -11
  10. package/skills/standard/self-review.SKILL.md +50 -10
  11. package/skills/standard/superpower.SKILL.md +46 -8
  12. package/skills/standard/tdd.SKILL.md +85 -12
  13. package/skills/standard/web-search.SKILL.md +65 -13
  14. package/src/agent/sub-agent.ts +0 -1
  15. package/src/artifacts/manifest.ts +101 -0
  16. package/src/artifacts/server.ts +343 -0
  17. package/src/core/context.ts +73 -13
  18. package/src/core/engine.ts +194 -71
  19. package/src/core/instructions.ts +1 -1
  20. package/src/core/permission.ts +35 -4
  21. package/src/core/session-store.ts +5 -1
  22. package/src/index.tsx +35 -2
  23. package/src/mcp/transport.ts +42 -5
  24. package/src/providers/anthropic.ts +2 -1
  25. package/src/providers/fetch-utils.ts +101 -0
  26. package/src/providers/openai-compat.ts +53 -15
  27. package/src/providers/registry.ts +1 -0
  28. package/src/shared/constants.ts +8 -1
  29. package/src/shared/types.ts +21 -1
  30. package/src/skills/loader.ts +2 -2
  31. package/src/tools/artifact/artifact.ts +144 -0
  32. package/src/tools/index.ts +3 -0
  33. package/src/ui/app.tsx +122 -15
  34. package/src/ui/chat.tsx +29 -7
  35. package/src/ui/commands.ts +117 -19
  36. package/src/ui/input.tsx +117 -35
  37. package/src/ui/picker.tsx +2 -5
@@ -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
- PACKAGE_NAME,
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, existsSync } = await import('node:fs')
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 mcpServers = ctx.config.skills?.mcpServers ?? []
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 (mcpServers.length > 0) {
1876
- lines.push(`Configured servers (${mcpServers.length}):`)
1869
+ if (configuredServers.length > 0) {
1870
+ lines.push(`Configured servers (${configuredServers.length}):`)
1877
1871
  lines.push('')
1878
- for (const s of mcpServers) {
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
- lines.push(` 📡 ${s.name}`)
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('── Status ──')
1912
- lines.push('MCP stdio protocol: full implementation in M3 milestone.')
1913
- lines.push('Current: config parsing + server registry ready.')
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
- // ── Creative status messages (inspired by Claude Code's whimsical verbs) ──
10
+ // ── Status verbs English (Claude Code-aligned) + Chinese (Mipham originals) ──
11
11
 
12
- const STATUS_VERBS = [
13
- '思忖中', // Pondering
14
- '推演中', // Deducting
15
- '凝炼中', // Condensing
16
- '熬煮中', // Simmering
17
- '锻造中', // Forging
18
- '研磨中', // Grinding
19
- '解构中', // Deconstructing
20
- '冥思中', // Contemplating
21
- '编织中', // Weaving
22
- '萃取中', // Extracting
23
- '烹制中', // Cooking
24
- '淬火中', // Quenching
25
- '雕琢中', // Chiseling
26
- '融汇中', // Fusing
27
- '觉照中', // Illuminating
28
- '运转中', // Operating
29
- '参详中', // Studying
30
- '化合中', // Synthesizing
31
- '浸润中', // Infusing
32
- '焙烤中', // Baking
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 STATUS_EMOJI = ['🔮', '⚙️', '💎', '🔥', '🧪', '🌊', '⚡', '🌀', '🎯', '💡']
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(STATUS_VERBS))
44
- const [emoji, setEmoji] = useState(() => pick(STATUS_EMOJI))
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 status messages while loading
113
+ // Rotate gerunds while loading
47
114
  useEffect(() => {
48
115
  if (!isLoading) return
49
116
  const interval = setInterval(() => {
50
- setVerb(pick(STATUS_VERBS))
51
- setEmoji(pick(STATUS_EMOJI))
52
- }, 2000)
117
+ setVerb(pick(STATUS_GERUNDS))
118
+ }, 1200)
53
119
  return () => clearInterval(interval)
54
120
  }, [isLoading])
55
121
 
56
- // Reset when loading starts
122
+ // Pick a fresh gerund when loading starts
57
123
  useEffect(() => {
58
124
  if (isLoading) {
59
- setVerb(pick(STATUS_VERBS))
60
- setEmoji(pick(STATUS_EMOJI))
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'}>{isLoading ? `${emoji}` : ''}</Text>
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={isLoading ? `${verb}...` : 'Type a message (Esc to exit)...'}
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, useEffect } from 'react'
1
+ import React, { useState, useCallback } from 'react'
2
2
  import { Box, Text, useInput } from 'ink'
3
- import type { MiphamConfig, ProviderConfig, ModelInfo } from '../shared/index.ts'
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 */}