@miphamai/cli 0.15.0 → 0.16.1

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/dist/mipham ADDED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.15.0",
3
+ "version": "0.16.1",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -42,13 +42,11 @@
42
42
  "test": "vitest run"
43
43
  },
44
44
  "dependencies": {
45
- "commander": "^13.1.0",
46
45
  "ink": "^5.2.1",
47
46
  "ink-text-input": "^6.0.0",
48
47
  "react": "^18.3.1",
49
48
  "react-devtools-core": "^4.28.5",
50
- "yaml": "^2.9.0",
51
- "zod": "^3.25.76"
49
+ "yaml": "^2.9.0"
52
50
  },
53
51
  "devDependencies": {
54
52
  "@mipham/shared": "workspace:*",
@@ -11,6 +11,8 @@
11
11
  * const unread = bus.poll('bg-1') // messages addressed TO bg-1
12
12
  */
13
13
 
14
+ export type AgentMessageType = 'message' | 'warning' | 'error'
15
+
14
16
  export interface AgentMessage {
15
17
  id: string
16
18
  from: string
@@ -19,6 +21,7 @@ export interface AgentMessage {
19
21
  message: string
20
22
  timestamp: Date
21
23
  read: boolean
24
+ type: AgentMessageType
22
25
  }
23
26
 
24
27
  export class AgentMessageBus {
@@ -29,7 +32,13 @@ export class AgentMessageBus {
29
32
  * Post a message from one agent to another.
30
33
  * Returns the message ID.
31
34
  */
32
- post(from: string, to: string, summary: string, message: string): string {
35
+ post(
36
+ from: string,
37
+ to: string,
38
+ summary: string,
39
+ message: string,
40
+ type: AgentMessageType = 'message',
41
+ ): string {
33
42
  const id = `msg-${++this.idCounter}`
34
43
  this.messages.push({
35
44
  id,
@@ -39,6 +48,7 @@ export class AgentMessageBus {
39
48
  message,
40
49
  timestamp: new Date(),
41
50
  read: false,
51
+ type,
42
52
  })
43
53
  return id
44
54
  }
@@ -91,6 +101,14 @@ export class AgentMessageBus {
91
101
  return this.messages.filter((m) => m.to === agentId && !m.read).length
92
102
  }
93
103
 
104
+ /**
105
+ * Get all unread warning messages for an agent.
106
+ * Does NOT mark them as read — use markRead() for that.
107
+ */
108
+ getWarnings(agentId: string): AgentMessage[] {
109
+ return this.messages.filter((m) => m.to === agentId && m.type === 'warning' && !m.read)
110
+ }
111
+
94
112
  /**
95
113
  * Prune messages older than maxAgeMs. Returns the number pruned.
96
114
  */
@@ -3,6 +3,7 @@ import type { ToolDefinition } from '../shared/index.ts'
3
3
  import type { SubAgentType, SubAgentOptions, AgentDefinition } from './types'
4
4
  import { createAgentContext } from './agent-context'
5
5
  import { getBackgroundAgentRegistry } from './background-registry'
6
+ import { getMessageBus } from './message-bus'
6
7
  import type { HookEngine } from '../core/hooks'
7
8
  import type { PermissionSystem } from '../core/permission'
8
9
 
@@ -156,6 +157,28 @@ export class SubAgent {
156
157
  // 'inherit' means use parent model
157
158
  const resolvedModel = modelToUse === 'inherit' ? model : modelToUse
158
159
 
160
+ // Validate resolved model exists in registry; fall back to parent model with warning
161
+ let finalModel = resolvedModel
162
+ if (resolvedModel !== model) {
163
+ const modelExists = this.registry.findModel(resolvedModel) !== undefined
164
+ if (!modelExists) {
165
+ const warnMsg = `Warning: model "${resolvedModel}" not found in provider registry. Falling back to "${model}".`
166
+ console.warn(warnMsg)
167
+
168
+ // Post warning to message bus for UI display
169
+ const bus = getMessageBus()
170
+ bus.post(
171
+ 'system',
172
+ 'main',
173
+ `Sub-agent model fallback: ${resolvedModel} → ${model}`,
174
+ warnMsg,
175
+ 'warning',
176
+ )
177
+
178
+ finalModel = model
179
+ }
180
+ }
181
+
159
182
  const chunks: string[] = []
160
183
  const MAX_TOOL_TURNS = options.maxTurns || 5
161
184
 
@@ -173,7 +196,7 @@ export class SubAgent {
173
196
  let turnText = ''
174
197
 
175
198
  for await (const chunk of provider.chat({
176
- model: resolvedModel,
199
+ model: finalModel,
177
200
  messages: currentMessages,
178
201
  systemPrompt: currentSystemPrompt,
179
202
  tools: toolDefs,
@@ -251,7 +274,7 @@ export class SubAgent {
251
274
  cwd: execCwd,
252
275
  sessionId: 'sub-agent',
253
276
  provider: '',
254
- model: resolvedModel,
277
+ model: finalModel,
255
278
  })
256
279
 
257
280
  currentMessages.push({
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Mipham Code — Environment Slash Commands
3
+ *
4
+ * Extracted from ui/commands.ts (2026-08-06).
5
+ * Handlers for: /theme, /release-notes, /ide, /terminal-setup
6
+ */
7
+ import type { CommandHandler } from '../ui/commands.js'
8
+ import { stripIndent } from '../ui/strip-indent.js'
9
+ import { NPM_INSTALL_COMMAND, NPM_UPDATE_COMMAND, PACKAGE_VERSION } from '../shared/index.ts'
10
+
11
+ export { themeCmd, releaseNotesCmd, ideCmd, terminalSetupCmd }
12
+
13
+ const themeCmd: CommandHandler = (_ctx, args) => {
14
+ const theme = args[0]?.toLowerCase()
15
+ const validThemes = ['dark', 'light', 'auto'] as const
16
+
17
+ if (!theme || !validThemes.includes(theme as (typeof validThemes)[number])) {
18
+ return {
19
+ content: `── Theme ──
20
+
21
+ Terminal color themes (set in .mipham/config.yml):
22
+
23
+ theme: dark — dark background (default, recommended for terminals)
24
+ theme: light — light background
25
+ theme: auto — follow system preference
26
+
27
+ Usage: /theme dark | light | auto
28
+
29
+ Current: auto (follows terminal)`,
30
+ }
31
+ }
32
+
33
+ return {
34
+ content: `✓ Theme set to "${theme}".
35
+
36
+ Add to ~/.mipham/config.yml to persist:
37
+ theme: ${theme}
38
+
39
+ Terminal themes affect syntax highlighting and UI accents.
40
+ Full theme customization is available in the Web UI at https://mipham.ai/code/dashboard`,
41
+ }
42
+ }
43
+
44
+ // ═══════════════════════════════════════════════════════════════
45
+ // Add-Dir — add a directory to workspace permissions
46
+ // ═══════════════════════════════════════════════════════════════
47
+
48
+ // ═══════════════════════════════════════════════════════════════
49
+ // Security / Audit — security review checklist
50
+ // ═══════════════════════════════════════════════════════════════
51
+
52
+ // ═══════════════════════════════════════════════════════════════
53
+ // Release Notes — version changelog
54
+ // ═══════════════════════════════════════════════════════════════
55
+
56
+ const releaseNotesCmd: CommandHandler = () => ({
57
+ content: `── Release Notes ──
58
+
59
+ v0.1.2 (2026-06-09) — Current
60
+ • 48 slash commands (up from 20)
61
+ • /setup guided project initialization wizard
62
+ • Checkpoint/rewind mechanism
63
+ • Focus mode (last exchange view)
64
+ • 3-level MIPHAM.md architecture
65
+ • /doctor system diagnostics
66
+ • /export conversation to file
67
+ • /review and /pr-comments code review workflows
68
+ • /memory management
69
+ • /upgrade instructions
70
+ • Clipboard support (macOS/Windows)
71
+
72
+ v0.1.1 (2026-06-02)
73
+ • Inline shared module for standalone builds
74
+ • Bin path fix for npm compatibility
75
+
76
+ v0.1.0 (2026-06-01)
77
+ • Initial release
78
+ • Multi-model support (7 providers)
79
+ • 16 built-in tools
80
+ • 11 skills (9 standard + 2 mipham)
81
+ • Ctrl+P interactive model picker
82
+ • SSE streaming support
83
+
84
+ Full changelog: https://mipham.ai/code/releases`,
85
+ })
86
+
87
+ // ═══════════════════════════════════════════════════════════════
88
+ // IDE — IDE integration guide
89
+ // ═══════════════════════════════════════════════════════════════
90
+
91
+ const ideCmd: CommandHandler = async (_ctx) => {
92
+ const { mkdirSync, writeFileSync, existsSync } = await import('node:fs')
93
+ const { join } = await import('node:path')
94
+
95
+ const cwd = process.cwd()
96
+ const vscodeDir = join(cwd, '.vscode')
97
+ mkdirSync(vscodeDir, { recursive: true })
98
+
99
+ const files: string[] = []
100
+
101
+ // ── Detect bun path ──
102
+ let bunPath = '/opt/homebrew/bin/bun'
103
+ try {
104
+ const { execSync } = await import('node:child_process')
105
+ const detected = execSync('which bun 2>/dev/null || echo /opt/homebrew/bin/bun', {
106
+ encoding: 'utf-8',
107
+ }).trim()
108
+ if (detected) bunPath = detected
109
+ } catch {
110
+ // Use default
111
+ }
112
+
113
+ // ── settings.json: terminal profile ──
114
+ const settingsPath = join(vscodeDir, 'settings.json')
115
+ const settings = {
116
+ 'terminal.integrated.profiles.osx': {
117
+ mipham: {
118
+ path: bunPath,
119
+ args: ['run', 'mipham'],
120
+ cwd: '${workspaceFolder}',
121
+ },
122
+ },
123
+ }
124
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8')
125
+ files.push('.vscode/settings.json')
126
+
127
+ // ── keybindings.json: Cmd+Esc launch ──
128
+ const keybindingsPath = join(vscodeDir, 'keybindings.json')
129
+ const keybindings = [
130
+ {
131
+ key: 'cmd+escape',
132
+ command: 'workbench.action.terminal.focus',
133
+ when: 'terminalProcessSupported',
134
+ },
135
+ {
136
+ key: 'cmd+shift+m',
137
+ command: 'workbench.action.terminal.new',
138
+ },
139
+ ]
140
+ writeFileSync(keybindingsPath, JSON.stringify(keybindings, null, 2) + '\n', 'utf-8')
141
+ files.push('.vscode/keybindings.json')
142
+
143
+ // ── extensions.json: recommended ──
144
+ const extensionsPath = join(vscodeDir, 'extensions.json')
145
+ const extensions = {
146
+ recommendations: ['miphamai.mipham-code'],
147
+ }
148
+ writeFileSync(extensionsPath, JSON.stringify(extensions, null, 2) + '\n', 'utf-8')
149
+ files.push('.vscode/extensions.json')
150
+
151
+ return {
152
+ content: [
153
+ '── VS Code Integration ──',
154
+ '',
155
+ `Generated in ${vscodeDir}:`,
156
+ ...files.map((f) => ` ✅ ${f}`),
157
+ '',
158
+ 'What was configured:',
159
+ ' • Terminal profile "mipham" — opens Mipham Code in integrated terminal',
160
+ ' • Keyboard shortcut Cmd+Esc — focus terminal',
161
+ ' • Cmd+Shift+M — new terminal',
162
+ '',
163
+ 'To use:',
164
+ ' 1. Restart VS Code (or reload window: Cmd+Shift+P → Reload Window)',
165
+ ' 2. Open terminal: Ctrl+` or Cmd+Esc',
166
+ ' 3. Select "mipham" profile from the terminal dropdown',
167
+ '',
168
+ 'Install the VS Code extension for full integration:',
169
+ ` code --install-extension miphamai.mipham-code`,
170
+ '',
171
+ 'JetBrains: Settings → Tools → Terminal → Shell path → bun run mipham',
172
+ ].join('\n'),
173
+ }
174
+ }
175
+
176
+ // ═══════════════════════════════════════════════════════════════
177
+ // Terminal Setup — shell integration guide
178
+ // ═══════════════════════════════════════════════════════════════
179
+
180
+ const terminalSetupCmd: CommandHandler = async () => {
181
+ const { writeFileSync, appendFileSync, existsSync, mkdirSync, readFileSync } =
182
+ await import('node:fs')
183
+ const { join } = await import('node:path')
184
+ const { homedir } = await import('node:os')
185
+
186
+ const home = homedir()
187
+ const lines: string[] = ['── Terminal Setup ──', '']
188
+
189
+ // ── 1. Generate standalone shell setup script ──
190
+ const miphamDir = join(home, '.mipham')
191
+ mkdirSync(miphamDir, { recursive: true })
192
+
193
+ const shellScript = join(miphamDir, 'shell-setup.sh')
194
+ const shellContent = [
195
+ '#!/bin/bash',
196
+ '# Mipham Code — Shell Integration',
197
+ '# Source this file in your shell profile:',
198
+ '# source ~/.mipham/shell-setup.sh',
199
+ '',
200
+ `export MIPHAM_HOME="${home}/.mipham"`,
201
+ '',
202
+ '# Alias: launch Mipham Code in current directory',
203
+ `alias mipham='cd $(pwd) && ${NPM_INSTALL_COMMAND} > /dev/null 2>&1; mipham'`,
204
+ '',
205
+ '# Or use the local development version:',
206
+ '# alias mipham="bun run /path/to/mipham-code/apps/cli/bin/mipham"',
207
+ '',
208
+ '# Auto-detect provider from config',
209
+ 'if [ -f ~/.mipham/config.yml ]; then',
210
+ ' export MIPHAM_PROVIDER=$(grep "defaultProvider:" ~/.mipham/config.yml | awk "{print \$2}")',
211
+ 'fi',
212
+ ].join('\n')
213
+ writeFileSync(shellScript, shellContent + '\n', 'utf-8')
214
+ lines.push(` ✅ Generated: ${shellScript}`)
215
+
216
+ // ── 2. Append to shell profile ──
217
+ const shell = process.env.SHELL || '/bin/zsh'
218
+ const profileName = shell.includes('zsh') ? '.zshrc' : '.bashrc'
219
+ const profilePath = join(home, profileName)
220
+ const sourceLine = `\n# Mipham Code shell integration\n[ -f ~/.mipham/shell-setup.sh ] && source ~/.mipham/shell-setup.sh\n`
221
+
222
+ try {
223
+ const existing = existsSync(profilePath) ? readFileSync(profilePath, 'utf-8') : ''
224
+ if (existing.includes('shell-setup.sh')) {
225
+ lines.push(` ⏭ ${profileName} already has Mipham Code integration`)
226
+ } else {
227
+ appendFileSync(profilePath, sourceLine, 'utf-8')
228
+ lines.push(` ✅ Added to ~/${profileName}`)
229
+ }
230
+ } catch {
231
+ lines.push(` ⚠️ Could not update ~/${profileName}. Add manually:`)
232
+ lines.push(` echo '${sourceLine.trim()}' >> ~/${profileName}`)
233
+ }
234
+
235
+ // ── 3. Global install check ──
236
+ lines.push('')
237
+ lines.push('── Installation ──')
238
+ try {
239
+ const { execSync } = await import('node:child_process')
240
+ const miphamPath = execSync('which mipham 2>/dev/null || echo ""', { encoding: 'utf-8' }).trim()
241
+ if (miphamPath) {
242
+ lines.push(` ✅ mipham found at: ${miphamPath}`)
243
+ const version = execSync('mipham --version 2>/dev/null || echo "unknown"', {
244
+ encoding: 'utf-8',
245
+ }).trim()
246
+ lines.push(` 📦 Version: ${version}`)
247
+ } else {
248
+ lines.push(` ⚠️ mipham not in PATH. Install globally:`)
249
+ lines.push(` ${NPM_INSTALL_COMMAND}`)
250
+ lines.push(` or: curl -fsSL https://mipham.ai/install.sh | bash`)
251
+ }
252
+ } catch {
253
+ lines.push(` 💡 Install: ${NPM_INSTALL_COMMAND}`)
254
+ }
255
+
256
+ // ── 4. Verify ──
257
+ lines.push('')
258
+ lines.push('── Next Steps ──')
259
+ lines.push(' 1. Restart your terminal or run: source ~/.mipham/shell-setup.sh')
260
+ lines.push(' 2. Run: mipham --version')
261
+ lines.push(' 3. Start coding: cd your-project && mipham')
262
+ lines.push('')
263
+ lines.push(`Works with: Zsh, Bash. Shell: ${shell}`)
264
+
265
+ return { content: lines.join('\n') }
266
+ }
267
+
268
+ // ═══════════════════════════════════════════════════════════════
269
+ // Phase 4 — MCP Server Management
270
+ // ═══════════════════════════════════════════════════════════════
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Mipham Code — Git & GitHub Slash Commands
3
+ *
4
+ * Extracted from ui/commands.ts (2026-08-06).
5
+ * Handlers for: /commit, /push, /pr, /issue
6
+ */
7
+ import type { CommandHandler } from '../ui/commands.js'
8
+ import { stripIndent } from '../ui/strip-indent.js'
9
+
10
+ export { commitCmd, pushCmd, prCmd, issueCmd }
11
+
12
+ /** Run a git command asynchronously — avoids blocking the TUI event loop. */
13
+ async function git(args: string[], timeoutMs = 5000): Promise<string> {
14
+ const proc = Bun.spawn(['git', ...args], {
15
+ stdout: 'pipe',
16
+ stderr: 'pipe',
17
+ })
18
+ const exitCode = await Promise.race([
19
+ proc.exited,
20
+ new Promise<number>((resolve) => setTimeout(() => resolve(143), timeoutMs)),
21
+ ])
22
+ if (exitCode === 143) {
23
+ proc.kill()
24
+ return ''
25
+ }
26
+ const stdout = await new Response(proc.stdout).text()
27
+ return exitCode === 0 ? stdout.trim() : ''
28
+ }
29
+
30
+ const commitCmd: CommandHandler = async () => {
31
+ try {
32
+ const diff = await git(['diff', '--cached', '--stat'])
33
+ const unstaged = await git(['diff', '--name-only'])
34
+
35
+ if (!diff) {
36
+ let msg = '── Git Commit ──\n\nNo staged changes found.\n\n'
37
+ if (unstaged) {
38
+ msg +=
39
+ 'Unstaged changes exist:\n' +
40
+ unstaged
41
+ .split('\n')
42
+ .map((f) => ' • ' + f)
43
+ .join('\n') +
44
+ '\n\n'
45
+ }
46
+ msg +=
47
+ 'Run: git add <files> to stage changes first.\nOr type: "commit these changes" for AI assistance.'
48
+ return { content: msg }
49
+ }
50
+
51
+ return {
52
+ content: [
53
+ '── Git Commit ──',
54
+ '',
55
+ 'Staged changes:',
56
+ diff,
57
+ '',
58
+ 'To generate a commit message, type: "write a commit message for these changes"',
59
+ 'To commit: git commit -m "your message"',
60
+ '',
61
+ 'Tip: Use Conventional Commits format (feat:, fix:, chore:, docs:)',
62
+ ].join('\n'),
63
+ }
64
+ } catch {
65
+ return { content: '── Git Commit ──\n\nCould not run git. Are you in a git repository?' }
66
+ }
67
+ }
68
+
69
+ const pushCmd: CommandHandler = async () => {
70
+ try {
71
+ const branch = await git(['branch', '--show-current'], 3000)
72
+ const aheadRaw = await git(['rev-list', '--count', `origin/${branch}..HEAD`], 3000)
73
+ const ahead = aheadRaw || '0'
74
+
75
+ return {
76
+ content: [
77
+ '── Git Push ──',
78
+ '',
79
+ `Branch: ${branch || '(unknown)'}`,
80
+ `Commits ahead of remote: ${ahead}`,
81
+ '',
82
+ ahead !== '0'
83
+ ? `Ready to push ${ahead} commit(s). Run: git push origin ${branch}`
84
+ : 'Nothing to push (up to date).',
85
+ '',
86
+ 'Or type: "push my changes" for AI assistance.',
87
+ ].join('\n'),
88
+ }
89
+ } catch {
90
+ return { content: '── Git Push ──\n\nCould not determine git status.' }
91
+ }
92
+ }
93
+
94
+ const prCmd: CommandHandler = async () => {
95
+ try {
96
+ const branch = await git(['branch', '--show-current'], 3000)
97
+ const mainBranchRaw = await git(['remote', 'show', 'origin'], 3000)
98
+ const mainBranch = mainBranchRaw.match(/HEAD branch:\s*(\S+)/)?.[1] || 'main'
99
+
100
+ const diff =
101
+ (await git(['diff', '--stat', `origin/${mainBranch}...HEAD`], 5000)) ||
102
+ (await git(['diff', '--stat', `${mainBranch}...HEAD`], 5000))
103
+
104
+ const commits =
105
+ (await git(['log', '--oneline', `origin/${mainBranch}..HEAD`], 5000)) ||
106
+ (await git(['log', '--oneline', `${mainBranch}..HEAD`], 5000))
107
+
108
+ return {
109
+ content: [
110
+ '── Create Pull Request ──',
111
+ '',
112
+ `Branch: ${branch} → ${mainBranch}`,
113
+ '',
114
+ commits
115
+ ? `Commits:\n${commits
116
+ .split('\n')
117
+ .map((c) => ' ' + c)
118
+ .join('\n')}`
119
+ : 'No commits ahead.',
120
+ '',
121
+ diff ? `Changes:\n${diff}` : '',
122
+ '',
123
+ 'To create PR: type "create a pull request" for AI assistance.',
124
+ 'Or use: gh pr create --base ' + mainBranch + ' --head ' + branch,
125
+ ].join('\n'),
126
+ }
127
+ } catch {
128
+ return { content: '── Create Pull Request ──\n\nCould not determine PR context.' }
129
+ }
130
+ }
131
+
132
+ const issueCmd: CommandHandler = async () => {
133
+ return {
134
+ content: [
135
+ '── GitHub Issues ──',
136
+ '',
137
+ 'To file an issue, type: "create a GitHub issue for [description]"',
138
+ '',
139
+ 'Quick commands:',
140
+ ' gh issue create --title "..." --body "..."',
141
+ ' gh issue list',
142
+ '',
143
+ 'Use /github-ops skill for advanced GitHub automation.',
144
+ ].join('\n'),
145
+ }
146
+ }
147
+
148
+ // ═══════════════════════════════════════════════════════════════
149
+ // Code Quality — bridge slash commands to skills (Claude Code parity)
150
+ // ═══════════════════════════════════════════════════════════════
151
+
152
+ /** Factory for git-diff-based commands that bridge to AI skills via forwardToAI. */