@miphamai/cli 0.5.8 → 0.5.10
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/LICENSE +201 -0
- package/README.md +42 -37
- package/bin/mipham.ts +95 -0
- package/dist/mipham +0 -0
- package/package.json +10 -10
- package/src/config/loader.ts +178 -14
- package/src/plugin/plugin-manager.ts +77 -0
- package/src/providers/anthropic.ts +12 -2
- package/src/providers/bootstrap.ts +11 -1
- package/src/providers/openai-compat.ts +17 -4
- package/src/shared/constants.ts +45 -0
- package/src/shared/update.ts +158 -0
- package/src/skills/registry.ts +325 -0
- package/src/ui/chat.tsx +11 -1
- package/src/ui/command-picker.tsx +145 -0
- package/src/ui/commands.ts +459 -38
- package/src/ui/input.tsx +42 -5
package/src/ui/commands.ts
CHANGED
|
@@ -98,6 +98,9 @@ const helpCmd: CommandHandler = (ctx) => {
|
|
|
98
98
|
/tools List available tools (${toolsCount} total)
|
|
99
99
|
/skills List loaded skills (${skillCount} built-in)
|
|
100
100
|
/reload-skills Reload all skills
|
|
101
|
+
/browse-skills Browse community skill marketplace
|
|
102
|
+
/install-skill Install a skill by name or URL
|
|
103
|
+
/remove-skill Remove an installed skill
|
|
101
104
|
/commands List all slash commands
|
|
102
105
|
/mcp MCP server status
|
|
103
106
|
|
|
@@ -117,6 +120,7 @@ const helpCmd: CommandHandler = (ctx) => {
|
|
|
117
120
|
── Project ─────────────────────────
|
|
118
121
|
/init Initialize .mipham config
|
|
119
122
|
/setup Guided project setup wizard
|
|
123
|
+
/recommend Analyze project + recommend setup
|
|
120
124
|
/permissions Show permission settings
|
|
121
125
|
/add-dir <dir> Add workspace directory
|
|
122
126
|
/security Security review checklist
|
|
@@ -394,24 +398,89 @@ const todosCmd: CommandHandler = (_ctx) => ({
|
|
|
394
398
|
// Project
|
|
395
399
|
// ═══════════════════════════════════════════════════════════════
|
|
396
400
|
|
|
397
|
-
const initCmd: CommandHandler = (
|
|
398
|
-
|
|
399
|
-
|
|
401
|
+
const initCmd: CommandHandler = async (ctx) => {
|
|
402
|
+
const { existsSync, mkdirSync, writeFileSync } = await import('node:fs')
|
|
403
|
+
const { join } = await import('node:path')
|
|
404
|
+
const { homedir } = await import('node:os')
|
|
405
|
+
|
|
406
|
+
const home = homedir()
|
|
407
|
+
const userConfigPath = join(home, '.mipham', 'config.yml')
|
|
408
|
+
|
|
409
|
+
// Generate user-friendly config if it doesn't exist yet
|
|
410
|
+
if (!existsSync(userConfigPath)) {
|
|
411
|
+
mkdirSync(join(home, '.mipham'), { recursive: true })
|
|
412
|
+
|
|
413
|
+
const activeProviders = ctx.config.providers.filter((p) => p.status === 'active')
|
|
414
|
+
const providerYaml = activeProviders
|
|
415
|
+
.map((p) => {
|
|
416
|
+
const tips: Record<string, string> = {
|
|
417
|
+
anthropic: '# Get key: https://console.anthropic.com/',
|
|
418
|
+
openai: '# Get key: https://platform.openai.com/api-keys',
|
|
419
|
+
deepseek: '# Get key: https://platform.deepseek.com/api_keys',
|
|
420
|
+
kimi: '# Get key: https://platform.moonshot.cn/',
|
|
421
|
+
doubao: '# Get key: https://console.volcengine.com/ark',
|
|
422
|
+
hunyuan: '# Get key: https://console.cloud.tencent.com/hunyuan',
|
|
423
|
+
qwen: '# Get key: https://dashscope.console.aliyun.com/apiKey',
|
|
424
|
+
google: '# Get key: https://aistudio.google.com/apikey',
|
|
425
|
+
}
|
|
426
|
+
const comment = tips[p.id] || ''
|
|
427
|
+
const baseUrlLine = p.baseUrl ? `\n baseUrl: "${p.baseUrl}"` : ''
|
|
428
|
+
return ` ${comment}
|
|
429
|
+
- id: ${p.id}
|
|
430
|
+
name: "${p.name}"${baseUrlLine}
|
|
431
|
+
apiKey: "\${${p.id.toUpperCase()}_API_KEY}"`
|
|
432
|
+
})
|
|
433
|
+
.join('\n\n')
|
|
434
|
+
|
|
435
|
+
const configContent = `# Mipham Code — User Configuration
|
|
436
|
+
# Location: ~/.mipham/config.yml
|
|
437
|
+
# Docs: https://mipham.ai/code/docs/config
|
|
438
|
+
#
|
|
439
|
+
# ═══ Quick Start ═══
|
|
440
|
+
# 1. Replace the API key placeholders below with your real keys
|
|
441
|
+
# 2. Save the file
|
|
442
|
+
# 3. Run 'mipham' — it auto-detects configured providers
|
|
443
|
+
#
|
|
444
|
+
# ═══ Environment Variables (Alternative) ═══
|
|
445
|
+
# Instead of editing this file, you can set env vars:
|
|
446
|
+
# export ANTHROPIC_API_KEY="sk-ant-..."
|
|
447
|
+
# export OPENAI_API_KEY="sk-..."
|
|
448
|
+
# (The \${VAR} syntax below reads from environment variables)
|
|
449
|
+
|
|
450
|
+
# ── Defaults ──
|
|
451
|
+
defaultProvider: ${ctx.providerId}
|
|
452
|
+
defaultModel: ${ctx.modelId}
|
|
453
|
+
permission: ask
|
|
400
454
|
|
|
401
|
-
|
|
455
|
+
# ── Providers (${activeProviders.length} pre-configured — just add your API keys) ──
|
|
456
|
+
providers:
|
|
457
|
+
${providerYaml}
|
|
458
|
+
`
|
|
402
459
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
defaultModel: claude-sonnet-4-6
|
|
407
|
-
permission: auto
|
|
460
|
+
writeFileSync(userConfigPath, configContent, 'utf-8')
|
|
461
|
+
return {
|
|
462
|
+
content: `✅ Mipham Code initialized!
|
|
408
463
|
|
|
409
|
-
|
|
410
|
-
Config set defaultProvider anthropic
|
|
464
|
+
Created: ~/.mipham/config.yml (${activeProviders.length} providers pre-configured)
|
|
411
465
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
466
|
+
Next steps:
|
|
467
|
+
1. Edit ~/.mipham/config.yml — replace API key placeholders with your real keys
|
|
468
|
+
2. Run mipham to start
|
|
469
|
+
|
|
470
|
+
Providers configured:
|
|
471
|
+
${activeProviders.map((p) => ` • ${p.name} — ${p.id.toUpperCase()}_API_KEY`).join('\n')}
|
|
472
|
+
|
|
473
|
+
Tip: /setup for the full 6-step wizard.`,
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Config already exists — show status
|
|
478
|
+
return {
|
|
479
|
+
content: `~/.mipham/config.yml already exists.
|
|
480
|
+
|
|
481
|
+
Run /setup for the full wizard, or /config to view current settings.`,
|
|
482
|
+
}
|
|
483
|
+
}
|
|
415
484
|
|
|
416
485
|
// ═══════════════════════════════════════════════════════════════
|
|
417
486
|
// Phase 1 — New Session Commands
|
|
@@ -520,6 +589,83 @@ const reloadSkillsCmd: CommandHandler = (ctx) => {
|
|
|
520
589
|
}
|
|
521
590
|
}
|
|
522
591
|
|
|
592
|
+
// ═══════════════════════════════════════════════════════════════
|
|
593
|
+
// Skill Marketplace — Community skill registry + installation
|
|
594
|
+
// ═══════════════════════════════════════════════════════════════
|
|
595
|
+
|
|
596
|
+
const browseSkillsCmd: CommandHandler = async () => {
|
|
597
|
+
const { getAvailableSkills, listInstalledSkills } = await import('../skills/registry')
|
|
598
|
+
const available = getAvailableSkills()
|
|
599
|
+
const installed = new Set(
|
|
600
|
+
listInstalledSkills().map((f: string) => f.replace(/\.(SKILL\.)?md$/i, '')),
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
const categories = new Map<string, string[]>()
|
|
604
|
+
for (const s of available) {
|
|
605
|
+
const list = categories.get(s.category) || []
|
|
606
|
+
list.push(s.name)
|
|
607
|
+
categories.set(s.category, list)
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const lines: string[] = [
|
|
611
|
+
'── Community Skills ──',
|
|
612
|
+
'',
|
|
613
|
+
`${available.length} skills available · ${installed.size} installed`,
|
|
614
|
+
'',
|
|
615
|
+
]
|
|
616
|
+
|
|
617
|
+
for (const [cat, names] of categories) {
|
|
618
|
+
lines.push(` ${cat}:`)
|
|
619
|
+
for (const name of names) {
|
|
620
|
+
const entry = available.find((s) => s.name === name)!
|
|
621
|
+
const marker = installed.has(name) ? '✅' : '⬜'
|
|
622
|
+
lines.push(` ${marker} /${name.padEnd(26)} ${entry.description}`)
|
|
623
|
+
}
|
|
624
|
+
lines.push('')
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
lines.push('Install: /install-skill <name>')
|
|
628
|
+
lines.push('Install from URL: /install-skill <github-url>')
|
|
629
|
+
lines.push('Remove: /remove-skill <name>')
|
|
630
|
+
|
|
631
|
+
return { content: lines.join('\n') }
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
const installSkillCmd: CommandHandler = async (_ctx, args) => {
|
|
635
|
+
const { installSkill, installSkillFromUrl } = await import('../skills/registry')
|
|
636
|
+
|
|
637
|
+
const target = args[0]
|
|
638
|
+
if (!target) {
|
|
639
|
+
return { content: 'Usage: /install-skill <skill-name> or /install-skill <url>' }
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
let result: { success: boolean; name: string; message: string }
|
|
643
|
+
|
|
644
|
+
if (target.startsWith('http://') || target.startsWith('https://')) {
|
|
645
|
+
result = installSkillFromUrl(target)
|
|
646
|
+
} else {
|
|
647
|
+
result = installSkill(target)
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return {
|
|
651
|
+
content: result.success ? `✅ ${result.message}` : `❌ ${result.message}`,
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
const removeSkillCmd: CommandHandler = async (_ctx, args) => {
|
|
656
|
+
const { removeSkill } = await import('../skills/registry')
|
|
657
|
+
|
|
658
|
+
const name = args[0]
|
|
659
|
+
if (!name) {
|
|
660
|
+
return { content: 'Usage: /remove-skill <skill-name>' }
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const result = removeSkill(name)
|
|
664
|
+
return {
|
|
665
|
+
content: result.success ? `✅ ${result.message}` : `❌ ${result.message}`,
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
523
669
|
// ═══════════════════════════════════════════════════════════════
|
|
524
670
|
// Phase 1 — History & Checkpoint Commands
|
|
525
671
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1147,24 +1293,72 @@ const memoryCmd: CommandHandler = async () => {
|
|
|
1147
1293
|
// Upgrade
|
|
1148
1294
|
// ═══════════════════════════════════════════════════════════════
|
|
1149
1295
|
|
|
1150
|
-
const upgradeCmd: CommandHandler = () =>
|
|
1151
|
-
|
|
1296
|
+
const upgradeCmd: CommandHandler = async () => {
|
|
1297
|
+
const { checkForUpdates, backupConfig, performUpdate, restoreConfig, getConfigPath } =
|
|
1298
|
+
await import('../shared/update')
|
|
1152
1299
|
|
|
1153
|
-
|
|
1300
|
+
const update = checkForUpdates()
|
|
1154
1301
|
|
|
1155
|
-
|
|
1156
|
-
|
|
1302
|
+
if (!update.available) {
|
|
1303
|
+
return {
|
|
1304
|
+
content: `── Upgrade Mipham Code ──
|
|
1157
1305
|
|
|
1158
|
-
|
|
1159
|
-
|
|
1306
|
+
Current version: v${update.current}
|
|
1307
|
+
Latest: v${update.latest}
|
|
1160
1308
|
|
|
1161
|
-
|
|
1162
|
-
• stable — recommended for most users
|
|
1163
|
-
• beta — early access to new features
|
|
1164
|
-
• nightly — latest commits, may be unstable
|
|
1309
|
+
✓ Already up to date.
|
|
1165
1310
|
|
|
1166
|
-
|
|
1167
|
-
}
|
|
1311
|
+
To check manually: https://www.npmjs.com/package/@miphamai/cli`,
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Update available — back up config and perform the upgrade
|
|
1316
|
+
const backupPath = backupConfig(`upgrade-v${update.current}`)
|
|
1317
|
+
|
|
1318
|
+
const lines: string[] = [
|
|
1319
|
+
'── Upgrade Mipham Code ──',
|
|
1320
|
+
'',
|
|
1321
|
+
`Current version: v${update.current}`,
|
|
1322
|
+
`Latest: v${update.latest}`,
|
|
1323
|
+
'',
|
|
1324
|
+
`→ New version available! Updating...`,
|
|
1325
|
+
'',
|
|
1326
|
+
]
|
|
1327
|
+
|
|
1328
|
+
if (backupPath) {
|
|
1329
|
+
lines.push(`Config backed up to: ${backupPath}`)
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const ok = performUpdate(update.latest)
|
|
1333
|
+
|
|
1334
|
+
if (ok) {
|
|
1335
|
+
const configPath = getConfigPath()
|
|
1336
|
+
const { existsSync } = await import('node:fs')
|
|
1337
|
+
lines.push('')
|
|
1338
|
+
lines.push(`✓ Updated to @miphamai/cli v${update.latest}`)
|
|
1339
|
+
|
|
1340
|
+
if (existsSync(configPath)) {
|
|
1341
|
+
lines.push(`✓ Config preserved: ${configPath}`)
|
|
1342
|
+
} else if (backupPath) {
|
|
1343
|
+
if (restoreConfig(backupPath)) {
|
|
1344
|
+
lines.push('✓ Config restored from backup.')
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
lines.push('')
|
|
1349
|
+
lines.push('⚠ The running Mipham Code process is still the old version.')
|
|
1350
|
+
lines.push(' Run `mipham` again to use the new version, or `mipham --version` to verify.')
|
|
1351
|
+
} else {
|
|
1352
|
+
lines.push('')
|
|
1353
|
+
lines.push('✗ Update failed.')
|
|
1354
|
+
lines.push(` Try manually: ${NPM_UPDATE_COMMAND}`)
|
|
1355
|
+
if (backupPath) {
|
|
1356
|
+
lines.push(` Your config backup is at: ${backupPath}`)
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
return { content: lines.join('\n') }
|
|
1361
|
+
}
|
|
1168
1362
|
|
|
1169
1363
|
// ═══════════════════════════════════════════════════════════════
|
|
1170
1364
|
// No-Plan — exit plan mode
|
|
@@ -1267,6 +1461,168 @@ Tool execution is sandboxed to the project directory by default.`,
|
|
|
1267
1461
|
}
|
|
1268
1462
|
}
|
|
1269
1463
|
|
|
1464
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1465
|
+
// Recommend — analyze project and recommend setup (like Claude Code
|
|
1466
|
+
// automation recommender)
|
|
1467
|
+
// ═══════════════════════════════════════════════════════════════
|
|
1468
|
+
|
|
1469
|
+
const recommendCmd: CommandHandler = async (ctx) => {
|
|
1470
|
+
const { existsSync, readFileSync } = await import('node:fs')
|
|
1471
|
+
const { join } = await import('node:path')
|
|
1472
|
+
const { getAvailableSkills } = await import('../skills/registry')
|
|
1473
|
+
|
|
1474
|
+
const cwd = process.cwd()
|
|
1475
|
+
const lines: string[] = ['── Mipham Code: Setup Recommendations ──', '', `Project: ${cwd}`, '']
|
|
1476
|
+
|
|
1477
|
+
// ── Detect project type ──
|
|
1478
|
+
const hasPackageJson = existsSync(join(cwd, 'package.json'))
|
|
1479
|
+
const hasTsConfig = existsSync(join(cwd, 'tsconfig.json'))
|
|
1480
|
+
const hasPyProject = existsSync(join(cwd, 'pyproject.toml'))
|
|
1481
|
+
const hasRequirements = existsSync(join(cwd, 'requirements.txt'))
|
|
1482
|
+
const hasDockerfile = existsSync(join(cwd, 'Dockerfile'))
|
|
1483
|
+
const hasGitHubActions = existsSync(join(cwd, '.github', 'workflows'))
|
|
1484
|
+
const hasNextConfig =
|
|
1485
|
+
existsSync(join(cwd, 'next.config.js')) || existsSync(join(cwd, 'next.config.ts'))
|
|
1486
|
+
const hasVueConfig =
|
|
1487
|
+
existsSync(join(cwd, 'vite.config.ts')) || existsSync(join(cwd, 'vite.config.js'))
|
|
1488
|
+
const hasTailwind =
|
|
1489
|
+
existsSync(join(cwd, 'tailwind.config.ts')) || existsSync(join(cwd, 'tailwind.config.js'))
|
|
1490
|
+
|
|
1491
|
+
let pkgData: Record<string, unknown> = {}
|
|
1492
|
+
if (hasPackageJson) {
|
|
1493
|
+
try {
|
|
1494
|
+
pkgData = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'))
|
|
1495
|
+
} catch {
|
|
1496
|
+
/* ignore */
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
const deps = {
|
|
1500
|
+
...((pkgData.dependencies as Record<string, string>) || {}),
|
|
1501
|
+
...((pkgData.devDependencies as Record<string, string>) || {}),
|
|
1502
|
+
}
|
|
1503
|
+
const isTypeScript = hasTsConfig || 'typescript' in deps
|
|
1504
|
+
const isReact = 'react' in deps || 'next' in deps
|
|
1505
|
+
const isVue = 'vue' in deps
|
|
1506
|
+
const isNode = hasPackageJson
|
|
1507
|
+
const isPython = hasPyProject || hasRequirements
|
|
1508
|
+
const isFastAPI = 'fastapi' in deps
|
|
1509
|
+
const isNextJS = 'next' in deps
|
|
1510
|
+
const isExpress = 'express' in deps || 'fastify' in deps
|
|
1511
|
+
const isDocker = hasDockerfile
|
|
1512
|
+
|
|
1513
|
+
// ── Detection summary ──
|
|
1514
|
+
const tags: string[] = []
|
|
1515
|
+
if (isTypeScript) tags.push('TypeScript')
|
|
1516
|
+
if (isReact) tags.push('React')
|
|
1517
|
+
if (isVue) tags.push('Vue')
|
|
1518
|
+
if (isNextJS) tags.push('Next.js')
|
|
1519
|
+
if (isExpress) tags.push('Node.js API')
|
|
1520
|
+
if (isFastAPI) tags.push('FastAPI')
|
|
1521
|
+
if (isPython) tags.push('Python')
|
|
1522
|
+
if (isDocker) tags.push('Docker')
|
|
1523
|
+
if (hasTailwind) tags.push('Tailwind CSS')
|
|
1524
|
+
if (hasGitHubActions) tags.push('CI/CD')
|
|
1525
|
+
|
|
1526
|
+
lines.push('── Detected Stack ──')
|
|
1527
|
+
lines.push('')
|
|
1528
|
+
if (tags.length > 0) {
|
|
1529
|
+
lines.push(` ${tags.join(' · ')}`)
|
|
1530
|
+
} else {
|
|
1531
|
+
lines.push(' (generic project — no specific framework detected)')
|
|
1532
|
+
}
|
|
1533
|
+
lines.push('')
|
|
1534
|
+
|
|
1535
|
+
// ── Skill recommendations ──
|
|
1536
|
+
const communitySkills = getAvailableSkills()
|
|
1537
|
+
const recommendedSkills: string[] = []
|
|
1538
|
+
|
|
1539
|
+
// Always useful
|
|
1540
|
+
recommendedSkills.push('code-review')
|
|
1541
|
+
recommendedSkills.push('systematic-debugging')
|
|
1542
|
+
|
|
1543
|
+
if (isTypeScript || isNode) {
|
|
1544
|
+
recommendedSkills.push('github-ops')
|
|
1545
|
+
}
|
|
1546
|
+
if (isReact || isVue || isNextJS) {
|
|
1547
|
+
recommendedSkills.push('frontend-design')
|
|
1548
|
+
}
|
|
1549
|
+
if (hasGitHubActions) {
|
|
1550
|
+
recommendedSkills.push('security-review')
|
|
1551
|
+
}
|
|
1552
|
+
if (isPython) {
|
|
1553
|
+
recommendedSkills.push('doc-generator')
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
// Filter to only those in the registry
|
|
1557
|
+
const available = recommendedSkills.filter((name) => communitySkills.some((s) => s.name === name))
|
|
1558
|
+
|
|
1559
|
+
lines.push('── Recommended Skills ──')
|
|
1560
|
+
lines.push('')
|
|
1561
|
+
if (available.length > 0) {
|
|
1562
|
+
for (const name of available) {
|
|
1563
|
+
const entry = communitySkills.find((s) => s.name === name)!
|
|
1564
|
+
lines.push(` /install-skill ${name.padEnd(26)} ${entry.description}`)
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
lines.push(' /browse-skills Browse all community skills')
|
|
1568
|
+
lines.push('')
|
|
1569
|
+
|
|
1570
|
+
// ── Provider recommendations ──
|
|
1571
|
+
const activeProviders = ctx.config.providers.filter((p) => p.status === 'active')
|
|
1572
|
+
const configured = activeProviders.filter((p) => p.apiKey && p.apiKey.trim() !== '')
|
|
1573
|
+
|
|
1574
|
+
lines.push('── Provider Status ──')
|
|
1575
|
+
lines.push('')
|
|
1576
|
+
if (configured.length === 0) {
|
|
1577
|
+
lines.push(' ⚠ No providers have API keys configured.')
|
|
1578
|
+
lines.push(' Run /setup 2 to configure providers.')
|
|
1579
|
+
lines.push('')
|
|
1580
|
+
lines.push(' Recommended for this project:')
|
|
1581
|
+
if (isTypeScript || isNode || isReact) {
|
|
1582
|
+
lines.push(' • anthropic — Claude (code generation, review)')
|
|
1583
|
+
lines.push(' • openai — GPT-5 (general purpose)')
|
|
1584
|
+
}
|
|
1585
|
+
if (isPython) {
|
|
1586
|
+
lines.push(' • anthropic — Claude (data science, ML)')
|
|
1587
|
+
}
|
|
1588
|
+
} else {
|
|
1589
|
+
lines.push(` ${configured.length}/${activeProviders.length} providers configured`)
|
|
1590
|
+
for (const p of configured) {
|
|
1591
|
+
lines.push(` ✅ ${p.id.padEnd(14)} ${p.name}`)
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
lines.push('')
|
|
1595
|
+
|
|
1596
|
+
// ── Config recommendations ──
|
|
1597
|
+
lines.push('── Configuration Tips ──')
|
|
1598
|
+
lines.push('')
|
|
1599
|
+
|
|
1600
|
+
const hasProjectMipham = existsSync(join(cwd, '.mipham'))
|
|
1601
|
+
if (!hasProjectMipham) {
|
|
1602
|
+
lines.push(' /setup 1 Initialize .mipham/ + MIPHAM.md + config.yml')
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
const { listInstalledSkills } = await import('../skills/registry')
|
|
1606
|
+
const installed = listInstalledSkills()
|
|
1607
|
+
if (installed.length === 0 && available.length > 0) {
|
|
1608
|
+
lines.push(' Tip: Install recommended skills above for better AI assistance')
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
if (isDocker && !hasProjectMipham) {
|
|
1612
|
+
lines.push(' Tip: Add .mipham/ to .dockerignore for smaller images')
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
if (hasGitHubActions) {
|
|
1616
|
+
lines.push(' /setup 5 Configure tool permissions for CI/CD safety')
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
lines.push(' /setup Full setup wizard (6 steps)')
|
|
1620
|
+
lines.push(' /help All commands')
|
|
1621
|
+
lines.push('')
|
|
1622
|
+
|
|
1623
|
+
return { content: lines.join('\n') }
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1270
1626
|
// ═══════════════════════════════════════════════════════════════
|
|
1271
1627
|
// Setup — guided project initialization (mirrors Claude Code /setup)
|
|
1272
1628
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1368,17 +1724,60 @@ async function setupStep1(ctx: CommandContext): Promise<CommandResult> {
|
|
|
1368
1724
|
|
|
1369
1725
|
// Create project config if missing
|
|
1370
1726
|
if (!existsSync(configPath)) {
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1727
|
+
// Generate a user-friendly config with all providers pre-populated.
|
|
1728
|
+
// Users just need to replace the API key placeholders with their real keys.
|
|
1729
|
+
const activeProviders = ctx.config.providers.filter((p) => p.status === 'active')
|
|
1730
|
+
const providerYaml = activeProviders
|
|
1731
|
+
.map((p) => {
|
|
1732
|
+
const comment =
|
|
1733
|
+
p.id === 'anthropic'
|
|
1734
|
+
? '# Get key: https://console.anthropic.com/'
|
|
1735
|
+
: p.id === 'openai'
|
|
1736
|
+
? '# Get key: https://platform.openai.com/api-keys'
|
|
1737
|
+
: p.id === 'deepseek'
|
|
1738
|
+
? '# Get key: https://platform.deepseek.com/api_keys'
|
|
1739
|
+
: p.id === 'kimi'
|
|
1740
|
+
? '# Get key: https://platform.moonshot.cn/'
|
|
1741
|
+
: p.id === 'doubao'
|
|
1742
|
+
? '# Get key: https://console.volcengine.com/ark'
|
|
1743
|
+
: p.id === 'hunyuan'
|
|
1744
|
+
? '# Get key: https://console.cloud.tencent.com/hunyuan'
|
|
1745
|
+
: p.id === 'qwen'
|
|
1746
|
+
? '# Get key: https://dashscope.console.aliyun.com/apiKey'
|
|
1747
|
+
: p.id === 'google'
|
|
1748
|
+
? '# Get key: https://aistudio.google.com/apikey'
|
|
1749
|
+
: ''
|
|
1750
|
+
const baseUrlLine = p.baseUrl ? `\n baseUrl: "${p.baseUrl}"` : ''
|
|
1751
|
+
return ` ${comment}
|
|
1752
|
+
- id: ${p.id}
|
|
1753
|
+
name: "${p.name}"${baseUrlLine}
|
|
1754
|
+
apiKey: "\${${p.id.toUpperCase()}_API_KEY}"`
|
|
1755
|
+
})
|
|
1756
|
+
.join('\n\n')
|
|
1757
|
+
|
|
1758
|
+
const defaultConfig = `# Mipham Code — User Configuration
|
|
1759
|
+
# Location: ~/.mipham/config.yml
|
|
1760
|
+
# Docs: https://mipham.ai/code/docs/config
|
|
1761
|
+
#
|
|
1762
|
+
# ═══ Quick Start ═══
|
|
1763
|
+
# 1. Set your API keys below (replace the placeholder values)
|
|
1764
|
+
# 2. Save the file
|
|
1765
|
+
# 3. Run 'mipham' — it auto-detects configured providers
|
|
1766
|
+
#
|
|
1767
|
+
# ═══ Environment Variables ═══
|
|
1768
|
+
# Instead of editing this file, you can set env vars:
|
|
1769
|
+
# export ANTHROPIC_API_KEY="sk-ant-..."
|
|
1770
|
+
# export OPENAI_API_KEY="sk-..."
|
|
1771
|
+
# (The \${VAR} syntax below reads from environment variables)
|
|
1772
|
+
|
|
1773
|
+
# ── Defaults ──
|
|
1374
1774
|
defaultProvider: ${ctx.providerId}
|
|
1375
1775
|
defaultModel: ${ctx.modelId}
|
|
1376
1776
|
permission: ask
|
|
1377
1777
|
|
|
1378
|
-
#
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
# apiKey: \$ANTHROPIC_API_KEY
|
|
1778
|
+
# ── Providers (8 configured, just add your API keys) ──
|
|
1779
|
+
providers:
|
|
1780
|
+
${providerYaml}
|
|
1382
1781
|
`
|
|
1383
1782
|
writeFileSync(configPath, defaultConfig, 'utf-8')
|
|
1384
1783
|
created.push('.mipham/config.yml')
|
|
@@ -1720,6 +2119,7 @@ const securityCmd: CommandHandler = async () => {
|
|
|
1720
2119
|
}
|
|
1721
2120
|
|
|
1722
2121
|
// 2. Check for hardcoded secrets (quick grep for common patterns)
|
|
2122
|
+
// ⚠ Security: results are redacted — only file:line locations are shown, never values
|
|
1723
2123
|
try {
|
|
1724
2124
|
const { execSync } = await import('node:child_process')
|
|
1725
2125
|
const secretPatterns = execSync(
|
|
@@ -1727,11 +2127,20 @@ const securityCmd: CommandHandler = async () => {
|
|
|
1727
2127
|
{ encoding: 'utf-8', timeout: 5000 },
|
|
1728
2128
|
).trim()
|
|
1729
2129
|
if (secretPatterns) {
|
|
2130
|
+
// Redact the actual values — only show file:line locations
|
|
2131
|
+
const redacted = secretPatterns
|
|
2132
|
+
.split('\n')
|
|
2133
|
+
.map((l) => {
|
|
2134
|
+
const colonIdx = l.indexOf(':')
|
|
2135
|
+
const secondColon = l.indexOf(':', colonIdx + 1)
|
|
2136
|
+
if (secondColon > 0) {
|
|
2137
|
+
return ' ' + l.slice(0, secondColon) + ' [VALUE REDACTED]'
|
|
2138
|
+
}
|
|
2139
|
+
return ' ' + l + ' [REDACTED]'
|
|
2140
|
+
})
|
|
2141
|
+
.join('\n')
|
|
1730
2142
|
findings.push(
|
|
1731
|
-
|
|
1732
|
-
.split('\n')
|
|
1733
|
-
.map((l) => ' ' + l)
|
|
1734
|
-
.join('\n')}`,
|
|
2143
|
+
`⚠ Hardcoded secrets detected (values redacted for security):\n${redacted}\n\n Replace with env vars: \${VAR_NAME} syntax`,
|
|
1735
2144
|
)
|
|
1736
2145
|
} else {
|
|
1737
2146
|
ok.push('No hardcoded secrets detected')
|
|
@@ -2344,6 +2753,9 @@ const commandsListCmd: CommandHandler = () => {
|
|
|
2344
2753
|
'/tools': 'Tools & Skills',
|
|
2345
2754
|
'/skills': 'Tools & Skills',
|
|
2346
2755
|
'/reload-skills': 'Tools & Skills',
|
|
2756
|
+
'/browse-skills': 'Tools & Skills',
|
|
2757
|
+
'/install-skill': 'Tools & Skills',
|
|
2758
|
+
'/remove-skill': 'Tools & Skills',
|
|
2347
2759
|
'/mcp': 'Tools & Skills',
|
|
2348
2760
|
'/commands': 'Tools & Skills',
|
|
2349
2761
|
'/plan': 'Workflow',
|
|
@@ -2360,6 +2772,7 @@ const commandsListCmd: CommandHandler = () => {
|
|
|
2360
2772
|
'/setup': 'Project',
|
|
2361
2773
|
'/permissions': 'Project',
|
|
2362
2774
|
'/add-dir': 'Project',
|
|
2775
|
+
'/recommend': 'Project',
|
|
2363
2776
|
'/security': 'Project',
|
|
2364
2777
|
'/audit': 'Project',
|
|
2365
2778
|
'/ide': 'Environment',
|
|
@@ -2437,6 +2850,9 @@ registry.set('/effort', effortCmd)
|
|
|
2437
2850
|
registry.set('/tools', toolsCmd)
|
|
2438
2851
|
registry.set('/skills', skillsCmd)
|
|
2439
2852
|
registry.set('/reload-skills', reloadSkillsCmd)
|
|
2853
|
+
registry.set('/browse-skills', browseSkillsCmd)
|
|
2854
|
+
registry.set('/install-skill', installSkillCmd)
|
|
2855
|
+
registry.set('/remove-skill', removeSkillCmd)
|
|
2440
2856
|
registry.set('/commands', commandsListCmd)
|
|
2441
2857
|
|
|
2442
2858
|
// Workflow
|
|
@@ -2461,6 +2877,7 @@ registry.set('/upgrade', upgradeCmd)
|
|
|
2461
2877
|
// Project
|
|
2462
2878
|
registry.set('/init', initCmd)
|
|
2463
2879
|
registry.set('/setup', setupCmd)
|
|
2880
|
+
registry.set('/recommend', recommendCmd)
|
|
2464
2881
|
registry.set('/permissions', permissionsCmd)
|
|
2465
2882
|
registry.set('/add-dir', addDirCmd)
|
|
2466
2883
|
registry.set('/security', securityCmd)
|
|
@@ -2540,6 +2957,9 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
2540
2957
|
'/tools': 'List available tools',
|
|
2541
2958
|
'/skills': 'List loaded skills',
|
|
2542
2959
|
'/reload-skills': 'Reload all skills',
|
|
2960
|
+
'/browse-skills': 'Browse community skill marketplace',
|
|
2961
|
+
'/install-skill': 'Install a skill by name or URL',
|
|
2962
|
+
'/remove-skill': 'Remove an installed skill',
|
|
2543
2963
|
'/mcp': 'MCP server status',
|
|
2544
2964
|
'/plan': 'Enter plan mode',
|
|
2545
2965
|
'/no-plan': 'Exit plan mode',
|
|
@@ -2555,6 +2975,7 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
|
|
|
2555
2975
|
'/setup': 'Guided project setup wizard',
|
|
2556
2976
|
'/permissions': 'Show permission settings',
|
|
2557
2977
|
'/add-dir': 'Add workspace directory',
|
|
2978
|
+
'/recommend': 'Analyze project + recommend skills & setup',
|
|
2558
2979
|
'/security': 'Security review checklist',
|
|
2559
2980
|
'/audit': 'Same as /security',
|
|
2560
2981
|
'/ide': 'IDE integration guide',
|
package/src/ui/input.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { Box, Text, useInput } from 'ink'
|
|
|
3
3
|
import TextInput from 'ink-text-input'
|
|
4
4
|
import { VimMotionEngine, type VimMode } from './vim-motions.js'
|
|
5
5
|
import { getCommandList, type CommandEntry } from './commands.js'
|
|
6
|
+
import { CommandPicker } from './command-picker.js'
|
|
6
7
|
|
|
7
8
|
interface InputBarProps {
|
|
8
9
|
onSubmit: (input: string) => void
|
|
@@ -252,10 +253,48 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
|
|
|
252
253
|
// The cursor hint is informational only; the user repositions manually.
|
|
253
254
|
})
|
|
254
255
|
|
|
256
|
+
// ── Command picker state ──
|
|
257
|
+
const [pickerActive, setPickerActive] = useState(false)
|
|
258
|
+
const prevValueRef = useRef(value)
|
|
259
|
+
|
|
260
|
+
// Auto-activate picker when user types "/"
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
if (value.startsWith('/') && !prevValueRef.current.startsWith('/') && vimMode === 'insert') {
|
|
263
|
+
setPickerActive(true)
|
|
264
|
+
}
|
|
265
|
+
// Dismiss picker when user clears the / prefix
|
|
266
|
+
if (!value.startsWith('/') && pickerActive) {
|
|
267
|
+
setPickerActive(false)
|
|
268
|
+
}
|
|
269
|
+
prevValueRef.current = value
|
|
270
|
+
}, [value, vimMode])
|
|
271
|
+
|
|
255
272
|
const handleSubmit = (val: string) => {
|
|
256
273
|
if (!val.trim() || isLoading) return
|
|
257
274
|
onSubmit(val)
|
|
258
275
|
setValue('')
|
|
276
|
+
setPickerActive(false)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// ── Picker mode: CommandPicker overlay ──
|
|
280
|
+
if (pickerActive && vimMode === 'insert') {
|
|
281
|
+
return (
|
|
282
|
+
<Box flexDirection="column" marginTop={1}>
|
|
283
|
+
<CommandPicker
|
|
284
|
+
initialFilter={value}
|
|
285
|
+
onSelect={(cmdName) => {
|
|
286
|
+
// Fill the command name and submit
|
|
287
|
+
onSubmit(cmdName)
|
|
288
|
+
setValue('')
|
|
289
|
+
setPickerActive(false)
|
|
290
|
+
}}
|
|
291
|
+
onClose={() => {
|
|
292
|
+
setPickerActive(false)
|
|
293
|
+
// Keep the current typed text so user can continue
|
|
294
|
+
}}
|
|
295
|
+
/>
|
|
296
|
+
</Box>
|
|
297
|
+
)
|
|
259
298
|
}
|
|
260
299
|
|
|
261
300
|
return (
|
|
@@ -287,18 +326,16 @@ export function InputBar({ onSubmit, isLoading }: InputBarProps) {
|
|
|
287
326
|
}
|
|
288
327
|
/>
|
|
289
328
|
</Box>
|
|
290
|
-
{/* Slash command hints — shown when typing / in INSERT mode */}
|
|
291
|
-
{slashHints.length > 0 && vimMode === 'insert' && (
|
|
292
|
-
<Box marginTop={1} flexDirection="
|
|
329
|
+
{/* Slash command hints — shown when typing / in INSERT mode (only when picker is NOT active) */}
|
|
330
|
+
{slashHints.length > 0 && vimMode === 'insert' && !pickerActive && (
|
|
331
|
+
<Box marginTop={1} flexDirection="column" gap={1}>
|
|
293
332
|
<Text dimColor>Commands: </Text>
|
|
294
333
|
{slashHints.map((cmd, i) => (
|
|
295
334
|
<Text key={cmd.name} color="cyan">
|
|
296
|
-
{i > 0 ? ' ' : ''}
|
|
297
335
|
{cmd.name}
|
|
298
336
|
</Text>
|
|
299
337
|
))}
|
|
300
338
|
<Text dimColor>
|
|
301
|
-
{' '}
|
|
302
339
|
(
|
|
303
340
|
{slashHints.length === allCommands.length
|
|
304
341
|
? 'all'
|