@miphamai/cli 0.5.8 → 0.5.9

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.
@@ -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 = (_ctx) => ({
398
- content: stripIndent`
399
- ── Initialize Mipham Code ──
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
- To initialize a project:
455
+ # ── Providers (${activeProviders.length} pre-configured — just add your API keys) ──
456
+ providers:
457
+ ${providerYaml}
458
+ `
402
459
 
403
- 1. Create .mipham/config.yml:
404
- version: "0.1.0"
405
- defaultProvider: anthropic
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
- 2. Or use the Config tool:
410
- Config set defaultProvider anthropic
464
+ Created: ~/.mipham/config.yml (${activeProviders.length} providers pre-configured)
411
465
 
412
- Run /config to view current configuration.
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
- content: `─ Upgrade Mipham Code
1296
+ const upgradeCmd: CommandHandler = async () => {
1297
+ const { checkForUpdates, backupConfig, performUpdate, restoreConfig, getConfigPath } =
1298
+ await import('../shared/update')
1152
1299
 
1153
- Current version: v0.1.0
1300
+ const update = checkForUpdates()
1154
1301
 
1155
- To upgrade:
1156
- curl -fsSL https://mipham.ai/install.sh | bash
1302
+ if (!update.available) {
1303
+ return {
1304
+ content: `── Upgrade Mipham Code ──
1157
1305
 
1158
- Or if installed via npm:
1159
- ${NPM_UPDATE_COMMAND}
1306
+ Current version: v${update.current}
1307
+ Latest: v${update.latest}
1160
1308
 
1161
- Release channels:
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
- Check for updates: https://mipham.ai/code/releases`,
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
- const defaultConfig = `# Mipham Code Project Configuration
1372
- # See https://mipham.ai/code/docs/config for all options
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
- # Uncomment to add project-specific providers:
1379
- # providers:
1380
- # - id: anthropic
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
- `Possible hardcoded secrets found:\n${secretPatterns
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="row" gap={1}>
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'