@miphamai/cli 0.5.11 → 0.6.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.
@@ -7,6 +7,7 @@
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 type { PluginManager } from '../plugin/plugin-manager'
10
11
  import { McpClient } from '../mcp/client'
11
12
  import { NPM_INSTALL_COMMAND, NPM_UPDATE_COMMAND, PACKAGE_VERSION } from '../shared/index.ts'
12
13
 
@@ -23,6 +24,7 @@ export interface CommandContext {
23
24
  setFocusMode: (on: boolean) => void
24
25
  setGoal: (text: string) => void
25
26
  skillsLoader?: SkillsLoader
27
+ pluginManager?: PluginManager
26
28
  }
27
29
 
28
30
  export interface CommandResult {
@@ -71,6 +73,10 @@ const helpCmd: CommandHandler = (ctx) => {
71
73
  /rename <name> Rename current session
72
74
  /goal <text> Set session goal
73
75
  /recap Summarize session so far
76
+ /summary Generate session summary
77
+ /stats Session usage statistics
78
+ /files List files in CWD
79
+ /cd <path> Change working directory
74
80
  /export Export conversation to file
75
81
  /doctor System diagnostics
76
82
  /resume List saved sessions
@@ -103,6 +109,12 @@ const helpCmd: CommandHandler = (ctx) => {
103
109
  /remove-skill Remove an installed skill
104
110
  /commands List all slash commands
105
111
  /mcp MCP server status
112
+ /plugins List installed plugins
113
+ /browse-plugins Browse community plugins
114
+ /install-plugin Install plugin from npm or path
115
+ /remove-plugin Remove an installed plugin
116
+ /plugin-enable Enable a disabled plugin
117
+ /plugin-disable Disable an enabled plugin
106
118
 
107
119
  ── Workflow ────────────────────────
108
120
  /plan Enter plan mode (read-only)
@@ -115,8 +127,21 @@ const helpCmd: CommandHandler = (ctx) => {
115
127
  /diff Show git diff
116
128
  /workflows List workflow scripts
117
129
  /loop <int> <p> Run prompt on interval
130
+ /loop init Scaffold .mipham/ vault structure
131
+ /batch Apply changes across files
132
+ /hooks Manage lifecycle hook scripts
118
133
  /schedule View scheduled tasks [stub]
119
134
 
135
+ ── Git & GitHub ─────────────────────
136
+ /commit Review staged changes + commit
137
+ /push Push current branch
138
+ /pr Create a pull request
139
+ /issue File a GitHub issue
140
+
141
+ ── Code Quality ─────────────────────
142
+ /simplify Review for reuse, quality, simplification
143
+ /lint Run project linter
144
+
120
145
  ── Project ─────────────────────────
121
146
  /init Initialize .mipham config
122
147
  /setup Guided project setup wizard
@@ -618,8 +643,9 @@ const browseSkillsCmd: CommandHandler = async () => {
618
643
  lines.push(` ${cat}:`)
619
644
  for (const name of names) {
620
645
  const entry = available.find((s) => s.name === name)!
621
- const marker = installed.has(name) ? '✅' : '⬜'
622
- lines.push(` ${marker} /${name.padEnd(26)} ${entry.description}`)
646
+ const marker = entry.builtin ? '🏠' : installed.has(name) ? '✅' : '⬜'
647
+ const status = entry.builtin ? ' (built-in)' : installed.has(name) ? ' (installed)' : ''
648
+ lines.push(` ${marker} /${name.padEnd(26)} ${entry.description}${status}`)
623
649
  }
624
650
  lines.push('')
625
651
  }
@@ -666,6 +692,181 @@ const removeSkillCmd: CommandHandler = async (_ctx, args) => {
666
692
  }
667
693
  }
668
694
 
695
+ // ═══════════════════════════════════════════════════════════════
696
+ // Plugins — Community plugin marketplace + local management
697
+ // ═══════════════════════════════════════════════════════════════
698
+
699
+ const pluginsCmd: CommandHandler = (ctx) => {
700
+ const manager = ctx.pluginManager
701
+ if (!manager) {
702
+ return { content: 'PluginManager not available in this session.' }
703
+ }
704
+
705
+ const plugins = manager.list()
706
+ if (plugins.length === 0) {
707
+ return {
708
+ content: [
709
+ '── Installed Plugins ──',
710
+ '',
711
+ 'No plugins installed.',
712
+ '',
713
+ 'Install plugins:',
714
+ ' /install-plugin <npm-package> Install from npm registry',
715
+ ' /browse-plugins Browse community plugins',
716
+ ' mipham plugin install <path> Install from local directory',
717
+ ].join('\n'),
718
+ }
719
+ }
720
+
721
+ const lines: string[] = [
722
+ '── Installed Plugins ──',
723
+ '',
724
+ ...plugins.map((p) => {
725
+ const status = p.enabled ? '✅ enabled' : '⛔ disabled'
726
+ return ` ${p.name.padEnd(24)} v${p.version.padEnd(10)} ${status} ${p.installedAt ? new Date(p.installedAt).toLocaleDateString() : ''}`
727
+ }),
728
+ '',
729
+ `${plugins.length} plugin(s) installed.`,
730
+ '',
731
+ 'Manage: /install-plugin | /remove-plugin | /plugin-enable | /plugin-disable',
732
+ ]
733
+ return { content: lines.join('\n') }
734
+ }
735
+
736
+ const browsePluginsCmd: CommandHandler = async (ctx) => {
737
+ const { getAvailablePlugins } = await import('../plugin/plugin-registry')
738
+ const available = getAvailablePlugins()
739
+ const installed = new Set((ctx.pluginManager?.list() ?? []).map((p) => p.name))
740
+
741
+ const categories = new Map<string, string[]>()
742
+ for (const p of available) {
743
+ const list = categories.get(p.category) || []
744
+ list.push(p.name)
745
+ categories.set(p.category, list)
746
+ }
747
+
748
+ const lines: string[] = [
749
+ '── Community Plugins ──',
750
+ '',
751
+ `${available.length} plugins available · ${installed.size} installed`,
752
+ '',
753
+ ]
754
+
755
+ for (const [cat, names] of categories) {
756
+ lines.push(` ${cat}:`)
757
+ for (const name of names) {
758
+ const entry = available.find((p) => p.name === name)!
759
+ const marker = installed.has(name) ? '✅' : '⬜'
760
+ lines.push(` ${marker} ${name.padEnd(22)} ${entry.description}`)
761
+ lines.push(` npm: ${entry.npmPackage}`)
762
+ }
763
+ lines.push('')
764
+ }
765
+
766
+ lines.push('Install: /install-plugin <npm-package>')
767
+ lines.push('Remove: /remove-plugin <name>')
768
+
769
+ return { content: lines.join('\n') }
770
+ }
771
+
772
+ const installPluginCmd: CommandHandler = async (ctx, args) => {
773
+ const target = args[0]
774
+ if (!target) {
775
+ return {
776
+ content: [
777
+ 'Usage: /install-plugin <npm-package> or /install-plugin <local-path>',
778
+ '',
779
+ 'Examples:',
780
+ ' /install-plugin @roomi-fields/notebooklm-mcp',
781
+ ' /install-plugin mipham-plugin-security',
782
+ ' /install-plugin ~/my-custom-plugin/',
783
+ '',
784
+ 'Use /browse-plugins to see available community plugins.',
785
+ ].join('\n'),
786
+ }
787
+ }
788
+
789
+ const manager = ctx.pluginManager
790
+ if (!manager) {
791
+ return { content: '❌ PluginManager not available in this session.' }
792
+ }
793
+
794
+ // Determine if it's a local path or npm package
795
+ const isLocalPath = target.startsWith('./') || target.startsWith('/') || target.startsWith('~/')
796
+
797
+ let result: { success: boolean; message: string }
798
+
799
+ if (isLocalPath) {
800
+ const { resolve } = await import('node:path')
801
+ const resolved = resolve(target.replace(/^~/, process.env.HOME || '~'))
802
+ result = manager.install(resolved)
803
+ } else {
804
+ // Treat as npm package name
805
+ result = manager.installFromNpm(target)
806
+ }
807
+
808
+ return {
809
+ content: result.success ? `✅ ${result.message}` : `❌ ${result.message}`,
810
+ }
811
+ }
812
+
813
+ const removePluginCmd: CommandHandler = (ctx, args) => {
814
+ const name = args[0]
815
+ if (!name) {
816
+ return { content: 'Usage: /remove-plugin <name>\n\nUse /plugins to see installed plugins.' }
817
+ }
818
+
819
+ const manager = ctx.pluginManager
820
+ if (!manager) {
821
+ return { content: '❌ PluginManager not available in this session.' }
822
+ }
823
+
824
+ const ok = manager.remove(name)
825
+ return {
826
+ content: ok
827
+ ? `✅ Plugin "${name}" removed.`
828
+ : `❌ Plugin "${name}" not found. Use /plugins to list installed plugins.`,
829
+ }
830
+ }
831
+
832
+ const pluginEnableCmd: CommandHandler = (ctx, args) => {
833
+ const name = args[0]
834
+ if (!name) {
835
+ return { content: 'Usage: /plugin-enable <name>\n\nUse /plugins to see installed plugins.' }
836
+ }
837
+
838
+ const manager = ctx.pluginManager
839
+ if (!manager) {
840
+ return { content: '❌ PluginManager not available in this session.' }
841
+ }
842
+
843
+ const ok = manager.enable(name)
844
+ return {
845
+ content: ok
846
+ ? `✅ Plugin "${name}" enabled.`
847
+ : `❌ Plugin "${name}" not found. Use /plugins to list installed plugins.`,
848
+ }
849
+ }
850
+
851
+ const pluginDisableCmd: CommandHandler = (ctx, args) => {
852
+ const name = args[0]
853
+ if (!name) {
854
+ return { content: 'Usage: /plugin-disable <name>\n\nUse /plugins to see installed plugins.' }
855
+ }
856
+
857
+ const manager = ctx.pluginManager
858
+ if (!manager) {
859
+ return { content: '❌ PluginManager not available in this session.' }
860
+ }
861
+
862
+ const ok = manager.disable(name)
863
+ return {
864
+ content: ok
865
+ ? `✅ Plugin "${name}" disabled.`
866
+ : `❌ Plugin "${name}" not found. Use /plugins to list installed plugins.`,
867
+ }
868
+ }
869
+
669
870
  // ═══════════════════════════════════════════════════════════════
670
871
  // Phase 1 — History & Checkpoint Commands
671
872
  // ═══════════════════════════════════════════════════════════════
@@ -887,13 +1088,74 @@ const branchCmd: CommandHandler = (ctx, args) => {
887
1088
  }
888
1089
  }
889
1090
 
890
- const loopCmd: CommandHandler = (_ctx, args) => {
1091
+ const loopCmd: CommandHandler = async (_ctx, args) => {
1092
+ const sub = args[0]
1093
+
1094
+ // ── /loop init — scaffold project vault ──
1095
+ if (sub === 'init' || sub === 'scaffold') {
1096
+ const targetPath = args[1] || process.cwd()
1097
+ const { scaffoldLoopKit } = await import('../commands/loop-scaffold')
1098
+ const { created, skipped } = scaffoldLoopKit(targetPath)
1099
+ const { resolve } = await import('node:path')
1100
+ const resolved = resolve(targetPath.replace(/^~/, process.env.HOME || '~'))
1101
+
1102
+ const lines: string[] = ['── LoopKit Vault Created ──', '', `Location: ${resolved}`, '']
1103
+
1104
+ if (created.length > 0) {
1105
+ lines.push(`Created (${created.length}):`)
1106
+ for (const c of created.slice(0, 12)) {
1107
+ const rel = c.replace(resolved + '/', '')
1108
+ lines.push(` ✅ ${rel}`)
1109
+ }
1110
+ if (created.length > 12) {
1111
+ lines.push(` ... and ${created.length - 12} more`)
1112
+ }
1113
+ }
1114
+
1115
+ if (skipped.length > 0) {
1116
+ lines.push('')
1117
+ lines.push(`Skipped (${skipped.length} — already exist):`)
1118
+ for (const s of skipped.slice(0, 5)) {
1119
+ const rel = s.replace(resolved + '/', '')
1120
+ lines.push(` ⏭ ${rel}`)
1121
+ }
1122
+ if (skipped.length > 5) {
1123
+ lines.push(` ... and ${skipped.length - 5} more`)
1124
+ }
1125
+ }
1126
+
1127
+ lines.push('')
1128
+ lines.push('── Structure ──')
1129
+ lines.push('')
1130
+ lines.push(' .mipham/ Mipham Code config directory')
1131
+ lines.push(' ├── CLAUDE.md Project AI instructions')
1132
+ lines.push(' ├── settings.json Permission & hook settings')
1133
+ lines.push(' ├── hooks/ Lifecycle hook scripts (3)')
1134
+ lines.push(' ├── agents/ Custom sub-agents')
1135
+ lines.push(' └── skills/ 9 domain-sorted skill directories')
1136
+ lines.push('')
1137
+ lines.push(' .mcp.json MCP server configuration')
1138
+ lines.push(' MEMORY.md AI persistent memory')
1139
+ lines.push(' run.sh / install.sh Project scripts')
1140
+ lines.push('')
1141
+ lines.push('Edit .mipham/CLAUDE.md to customize AI behavior.')
1142
+ lines.push('Add skills to .mipham/skills/<domain>/ directories.')
1143
+ lines.push('Configure MCP servers in .mcp.json.')
1144
+ lines.push('')
1145
+ lines.push('/loop init <path> to scaffold in a different location.')
1146
+
1147
+ return { content: lines.join('\n') }
1148
+ }
1149
+
1150
+ // ── /loop <interval> <prompt> — recurring prompt ──
891
1151
  if (args.length < 2) {
892
1152
  return {
893
1153
  content: stripIndent`
894
1154
  Usage: /loop <interval> <prompt>
1155
+ or: /loop init [path] Scaffold project vault structure
895
1156
 
896
- Run a prompt repeatedly on a set interval.
1157
+ Run a prompt repeatedly on a set interval, or initialize
1158
+ a LoopKit Vault project structure with Mipham Code config.
897
1159
 
898
1160
  Interval formats:
899
1161
  10s — 10 seconds
@@ -902,10 +1164,10 @@ const loopCmd: CommandHandler = (_ctx, args) => {
902
1164
 
903
1165
  Example:
904
1166
  /loop 5m check the deploy status
905
- /loop 1h run the smoke tests
1167
+ /loop init Create vault in current directory
1168
+ /loop init ~/my-project Create vault in specific directory
906
1169
 
907
1170
  Note: Continuous looping requires session persistence, coming in a future release.
908
- For now, this sets up the intent — you'll be prompted to confirm each iteration.
909
1171
  `,
910
1172
  }
911
1173
  }
@@ -1016,6 +1278,442 @@ const doctorCmd: CommandHandler = async (ctx) => {
1016
1278
  return { content: lines.join('\n') }
1017
1279
  }
1018
1280
 
1281
+ // ═══════════════════════════════════════════════════════════════
1282
+ // GitHub & Git Workflow Commands (Claude Code parity)
1283
+ // ═══════════════════════════════════════════════════════════════
1284
+
1285
+ const commitCmd: CommandHandler = async () => {
1286
+ try {
1287
+ const { execSync } = await import('node:child_process')
1288
+ const diff = execSync('git diff --cached --stat', { encoding: 'utf-8', timeout: 5000 }).trim()
1289
+ const unstaged = execSync('git diff --name-only', { encoding: 'utf-8', timeout: 5000 }).trim()
1290
+
1291
+ if (!diff) {
1292
+ let msg = '── Git Commit ──\n\nNo staged changes found.\n\n'
1293
+ if (unstaged) {
1294
+ msg +=
1295
+ 'Unstaged changes exist:\n' +
1296
+ unstaged
1297
+ .split('\n')
1298
+ .map((f) => ' • ' + f)
1299
+ .join('\n') +
1300
+ '\n\n'
1301
+ }
1302
+ msg +=
1303
+ 'Run: git add <files> to stage changes first.\nOr type: "commit these changes" for AI assistance.'
1304
+ return { content: msg }
1305
+ }
1306
+
1307
+ return {
1308
+ content: [
1309
+ '── Git Commit ──',
1310
+ '',
1311
+ 'Staged changes:',
1312
+ diff,
1313
+ '',
1314
+ 'To generate a commit message, type: "write a commit message for these changes"',
1315
+ 'To commit: git commit -m "your message"',
1316
+ '',
1317
+ 'Tip: Use Conventional Commits format (feat:, fix:, chore:, docs:)',
1318
+ ].join('\n'),
1319
+ }
1320
+ } catch {
1321
+ return { content: '── Git Commit ──\n\nCould not run git. Are you in a git repository?' }
1322
+ }
1323
+ }
1324
+
1325
+ const pushCmd: CommandHandler = async () => {
1326
+ try {
1327
+ const { execSync } = await import('node:child_process')
1328
+ const branch = execSync('git branch --show-current', {
1329
+ encoding: 'utf-8',
1330
+ timeout: 3000,
1331
+ }).trim()
1332
+ const ahead = execSync(`git rev-list --count origin/${branch}..HEAD 2>/dev/null || echo 0`, {
1333
+ encoding: 'utf-8',
1334
+ timeout: 3000,
1335
+ }).trim()
1336
+
1337
+ return {
1338
+ content: [
1339
+ '── Git Push ──',
1340
+ '',
1341
+ `Branch: ${branch || '(unknown)'}`,
1342
+ `Commits ahead of remote: ${ahead}`,
1343
+ '',
1344
+ ahead !== '0'
1345
+ ? `Ready to push ${ahead} commit(s). Run: git push origin ${branch}`
1346
+ : 'Nothing to push (up to date).',
1347
+ '',
1348
+ 'Or type: "push my changes" for AI assistance.',
1349
+ ].join('\n'),
1350
+ }
1351
+ } catch {
1352
+ return { content: '── Git Push ──\n\nCould not determine git status.' }
1353
+ }
1354
+ }
1355
+
1356
+ const prCmd: CommandHandler = async () => {
1357
+ try {
1358
+ const { execSync } = await import('node:child_process')
1359
+ const branch = execSync('git branch --show-current', {
1360
+ encoding: 'utf-8',
1361
+ timeout: 3000,
1362
+ }).trim()
1363
+ const mainBranch =
1364
+ execSync('git remote show origin 2>/dev/null | grep "HEAD branch" | cut -d: -f2', {
1365
+ encoding: 'utf-8',
1366
+ timeout: 3000,
1367
+ }).trim() || 'main'
1368
+
1369
+ const diff = execSync(
1370
+ `git diff --stat origin/${mainBranch}...HEAD 2>/dev/null || git diff --stat ${mainBranch}...HEAD 2>/dev/null || echo ""`,
1371
+ { encoding: 'utf-8', timeout: 5000 },
1372
+ ).trim()
1373
+
1374
+ const commits = execSync(
1375
+ `git log --oneline origin/${mainBranch}..HEAD 2>/dev/null || git log --oneline ${mainBranch}..HEAD 2>/dev/null || echo ""`,
1376
+ { encoding: 'utf-8', timeout: 5000 },
1377
+ ).trim()
1378
+
1379
+ return {
1380
+ content: [
1381
+ '── Create Pull Request ──',
1382
+ '',
1383
+ `Branch: ${branch} → ${mainBranch}`,
1384
+ '',
1385
+ commits
1386
+ ? `Commits:\n${commits
1387
+ .split('\n')
1388
+ .map((c) => ' ' + c)
1389
+ .join('\n')}`
1390
+ : 'No commits ahead.',
1391
+ '',
1392
+ diff ? `Changes:\n${diff}` : '',
1393
+ '',
1394
+ 'To create PR: type "create a pull request" for AI assistance.',
1395
+ 'Or use: gh pr create --base ' + mainBranch + ' --head ' + branch,
1396
+ ].join('\n'),
1397
+ }
1398
+ } catch {
1399
+ return { content: '── Create Pull Request ──\n\nCould not determine PR context.' }
1400
+ }
1401
+ }
1402
+
1403
+ const issueCmd: CommandHandler = async () => {
1404
+ return {
1405
+ content: [
1406
+ '── GitHub Issues ──',
1407
+ '',
1408
+ 'To file an issue, type: "create a GitHub issue for [description]"',
1409
+ '',
1410
+ 'Quick commands:',
1411
+ ' gh issue create --title "..." --body "..."',
1412
+ ' gh issue list',
1413
+ '',
1414
+ 'Use /github-ops skill for advanced GitHub automation.',
1415
+ ].join('\n'),
1416
+ }
1417
+ }
1418
+
1419
+ // ═══════════════════════════════════════════════════════════════
1420
+ // Code Quality Commands (Claude Code parity)
1421
+ // ═══════════════════════════════════════════════════════════════
1422
+
1423
+ const simplifyCmd: CommandHandler = async () => {
1424
+ try {
1425
+ const { execSync } = await import('node:child_process')
1426
+ const diff = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 }).trim()
1427
+ const changed = execSync('git diff --name-only', { encoding: 'utf-8', timeout: 3000 }).trim()
1428
+
1429
+ if (!diff) {
1430
+ return {
1431
+ content:
1432
+ '── Simplify ──\n\nNo uncommitted changes to review.\n\nRun /simplify after making changes to review for reuse, simplification, and quality improvements.',
1433
+ }
1434
+ }
1435
+
1436
+ return {
1437
+ content: [
1438
+ '── Simplify Review ──',
1439
+ '',
1440
+ 'Changed files:',
1441
+ diff,
1442
+ '',
1443
+ 'To run simplification review, type: "review these changes for simplification and quality"',
1444
+ '',
1445
+ 'This will check for:',
1446
+ ' • Code reuse opportunities',
1447
+ ' • Overly complex logic',
1448
+ ' • Dead code removal',
1449
+ ' • Style and consistency',
1450
+ '',
1451
+ `Files to review: ${changed.split('\n').length}`,
1452
+ ].join('\n'),
1453
+ }
1454
+ } catch {
1455
+ return { content: '── Simplify ──\n\nCould not detect changes. Are you in a git repository?' }
1456
+ }
1457
+ }
1458
+
1459
+ const lintCmd: CommandHandler = async () => {
1460
+ try {
1461
+ const { execSync } = await import('node:child_process')
1462
+ const { existsSync } = await import('node:fs')
1463
+ const { join } = await import('node:path')
1464
+ const cwd = process.cwd()
1465
+
1466
+ const hasPackageJson = existsSync(join(cwd, 'package.json'))
1467
+
1468
+ let lintOutput = ''
1469
+ if (hasPackageJson) {
1470
+ try {
1471
+ lintOutput = execSync(
1472
+ 'npx eslint . --ext .ts,.tsx,.js,.jsx --max-warnings 0 2>&1 || true',
1473
+ {
1474
+ encoding: 'utf-8',
1475
+ timeout: 30000,
1476
+ cwd,
1477
+ },
1478
+ ).trim()
1479
+ } catch {
1480
+ lintOutput = '(ESLint not configured or not found)'
1481
+ }
1482
+ } else {
1483
+ lintOutput = '(No package.json found — not a Node.js project)'
1484
+ }
1485
+
1486
+ const lines = lintOutput.split('\n').slice(0, 40)
1487
+ const truncated = lines.length >= 40 ? lines.join('\n') + '\n... (truncated)' : lines.join('\n')
1488
+
1489
+ return {
1490
+ content: [
1491
+ '── Lint Results ──',
1492
+ '',
1493
+ truncated || '(no issues found)',
1494
+ '',
1495
+ hasPackageJson
1496
+ ? 'To fix: type "fix the lint errors"'
1497
+ : 'Set up linting: https://mipham.ai/code/docs/linting',
1498
+ ].join('\n'),
1499
+ }
1500
+ } catch {
1501
+ return {
1502
+ content: '── Lint ──\n\nCould not run linter. Ensure ESLint is installed and configured.',
1503
+ }
1504
+ }
1505
+ }
1506
+
1507
+ // ═══════════════════════════════════════════════════════════════
1508
+ // Session Enhancement Commands (Claude Code parity)
1509
+ // ═══════════════════════════════════════════════════════════════
1510
+
1511
+ const filesCmd: CommandHandler = async () => {
1512
+ const { readdirSync, statSync } = await import('node:fs')
1513
+ const { join } = await import('node:path')
1514
+ const cwd = process.cwd()
1515
+
1516
+ try {
1517
+ const entries = readdirSync(cwd, { withFileTypes: true })
1518
+ const items = entries
1519
+ .filter((e) => !e.name.startsWith('.') || e.name === '.mipham' || e.name === '.mcp.json')
1520
+ .slice(0, 40)
1521
+ .map((e) => {
1522
+ const icon = e.isDirectory() ? '📁' : '📄'
1523
+ try {
1524
+ const stat = statSync(join(cwd, e.name))
1525
+ const size = stat.isFile() ? ` ${(stat.size / 1024).toFixed(1)}KB` : ''
1526
+ return ` ${icon} ${e.name}${size}`
1527
+ } catch {
1528
+ return ` ${icon} ${e.name}`
1529
+ }
1530
+ })
1531
+
1532
+ return {
1533
+ content: [
1534
+ '── Project Files ──',
1535
+ '',
1536
+ `CWD: ${cwd}`,
1537
+ '',
1538
+ ...items,
1539
+ entries.length > 40 ? ` ... and ${entries.length - 40} more files` : '',
1540
+ '',
1541
+ 'Use Read/Glob/Grep tools to examine files.',
1542
+ ].join('\n'),
1543
+ }
1544
+ } catch {
1545
+ return { content: '── Files ──\n\nCould not read directory.' }
1546
+ }
1547
+ }
1548
+
1549
+ const statsCmd: CommandHandler = (ctx) => {
1550
+ const c = ctx.engine.getContext()
1551
+ const msgs = c.getMessages()
1552
+ const tokens = c.getEstimatedTokens()
1553
+ const tools = ctx.engine.getTools()
1554
+
1555
+ const userMsgs = msgs.filter((m) => m.role === 'user').length
1556
+ const assistantMsgs = msgs.filter((m) => m.role === 'assistant').length
1557
+ const systemMsgs = msgs.filter((m) => m.role === 'system').length
1558
+
1559
+ return {
1560
+ content: [
1561
+ '── Session Stats ──',
1562
+ '',
1563
+ `Messages: ${msgs.length} (${userMsgs} user, ${assistantMsgs} AI, ${systemMsgs} system)`,
1564
+ `Tokens: ~${tokens.toLocaleString()} / 200,000`,
1565
+ `Tools available: ${tools.size}`,
1566
+ `Provider: ${ctx.providerId}`,
1567
+ `Model: ${ctx.modelId}`,
1568
+ `Permission: ${ctx.config.permission}`,
1569
+ '',
1570
+ `Usage: ${((tokens / 200_000) * 100).toFixed(1)}% of context window`,
1571
+ ].join('\n'),
1572
+ }
1573
+ }
1574
+
1575
+ const summaryCmd: CommandHandler = (ctx) => {
1576
+ const c = ctx.engine.getContext()
1577
+ const msgs = c.getMessages()
1578
+
1579
+ if (msgs.length === 0) {
1580
+ return { content: '── Summary ──\n\nNo conversation to summarize.' }
1581
+ }
1582
+
1583
+ const userMsgs = msgs
1584
+ .filter((m) => m.role === 'user' && typeof m.content === 'string')
1585
+ .slice(0, 5)
1586
+ .map((m) => {
1587
+ const text = typeof m.content === 'string' ? m.content : ''
1588
+ return text.length > 100 ? text.slice(0, 100) + '...' : text
1589
+ })
1590
+
1591
+ return {
1592
+ content: [
1593
+ '── Session Summary ──',
1594
+ '',
1595
+ `Total messages: ${msgs.length}`,
1596
+ `Est. tokens: ~${c.getEstimatedTokens().toLocaleString()}`,
1597
+ '',
1598
+ 'Recent topics:',
1599
+ ...userMsgs.map((t, i) => ` ${i + 1}. ${t}`),
1600
+ '',
1601
+ 'Use /export to save, /clear to reset.',
1602
+ ].join('\n'),
1603
+ }
1604
+ }
1605
+
1606
+ const cdCmd: CommandHandler = async (_ctx, args) => {
1607
+ const target = args[0]
1608
+ if (!target) {
1609
+ return {
1610
+ content: [
1611
+ 'Usage: /cd <path>',
1612
+ '',
1613
+ 'Change the session working directory.',
1614
+ 'Example: /cd ~/projects/my-app',
1615
+ '',
1616
+ `Current: ${process.cwd()}`,
1617
+ ].join('\n'),
1618
+ }
1619
+ }
1620
+
1621
+ const { existsSync } = await import('node:fs')
1622
+ const { resolve } = await import('node:path')
1623
+ const resolved = resolve(target.replace(/^~/, process.env.HOME || '~'))
1624
+
1625
+ if (!existsSync(resolved)) {
1626
+ return { content: `❌ Directory not found: ${resolved}` }
1627
+ }
1628
+
1629
+ try {
1630
+ process.chdir(resolved)
1631
+ return {
1632
+ content: [
1633
+ '── Directory Changed ──',
1634
+ '',
1635
+ `New CWD: ${resolved}`,
1636
+ '',
1637
+ 'The AI will now work relative to this directory.',
1638
+ 'Note: This changes the filesystem root for tools like Read, Write, Bash.',
1639
+ ].join('\n'),
1640
+ }
1641
+ } catch (err) {
1642
+ return { content: `❌ Failed to change directory: ${(err as Error).message}` }
1643
+ }
1644
+ }
1645
+
1646
+ const hooksCmd: CommandHandler = async () => {
1647
+ const { existsSync, readdirSync } = await import('node:fs')
1648
+ const { join } = await import('node:path')
1649
+ const cwd = process.cwd()
1650
+ const hooksDir = join(cwd, '.mipham', 'hooks')
1651
+
1652
+ if (!existsSync(hooksDir)) {
1653
+ return {
1654
+ content: [
1655
+ '── Hooks ──',
1656
+ '',
1657
+ 'No hooks configured.',
1658
+ '',
1659
+ 'Create hook scripts in .mipham/hooks/:',
1660
+ ' pre-tool-use.sh — runs before each tool',
1661
+ ' post-tool-use.sh — runs after each tool',
1662
+ ' stop.sh — runs when session ends',
1663
+ '',
1664
+ 'Use /loop init to scaffold hooks directory.',
1665
+ ].join('\n'),
1666
+ }
1667
+ }
1668
+
1669
+ try {
1670
+ const files = readdirSync(hooksDir).filter((f) => f.endsWith('.sh'))
1671
+ const lines: string[] = ['── Hooks ──', '', `Location: ${hooksDir}`, '']
1672
+
1673
+ for (const f of files) {
1674
+ try {
1675
+ const stat = (await import('node:fs')).statSync(join(hooksDir, f))
1676
+ const isExec = (stat.mode & 0o111) !== 0
1677
+ lines.push(
1678
+ ` ${isExec ? '✅' : '⚠️'} ${f}${isExec ? '' : ' (not executable — run chmod +x)'}`,
1679
+ )
1680
+ } catch {
1681
+ lines.push(` 📄 ${f}`)
1682
+ }
1683
+ }
1684
+
1685
+ lines.push('')
1686
+ lines.push(`${files.length} hook(s) found.`)
1687
+ return { content: lines.join('\n') }
1688
+ } catch {
1689
+ return { content: '── Hooks ──\n\nCould not read hooks directory.' }
1690
+ }
1691
+ }
1692
+
1693
+ const batchCmd: CommandHandler = async () => {
1694
+ return {
1695
+ content: [
1696
+ '── Batch Operations ──',
1697
+ '',
1698
+ 'Apply a change across multiple files or directories.',
1699
+ '',
1700
+ 'Usage: type "apply this change to all .ts files in src/"',
1701
+ '',
1702
+ 'The AI will:',
1703
+ ' 1. Understand your change description',
1704
+ ' 2. Find all matching files',
1705
+ ' 3. Apply the change consistently',
1706
+ ' 4. Report what was modified',
1707
+ '',
1708
+ 'Use this for:',
1709
+ ' • Renaming symbols across codebase',
1710
+ ' • Updating import patterns',
1711
+ ' • Applying consistent formatting changes',
1712
+ ' • Bulk config updates',
1713
+ ].join('\n'),
1714
+ }
1715
+ }
1716
+
1019
1717
  // ═══════════════════════════════════════════════════════════════
1020
1718
  // Export
1021
1719
  // ═══════════════════════════════════════════════════════════════
@@ -2708,6 +3406,7 @@ const commandsListCmd: CommandHandler = () => {
2708
3406
  History: [],
2709
3407
  'Model & Provider': [],
2710
3408
  'Tools & Skills': [],
3409
+ Plugins: [],
2711
3410
  Workflow: [],
2712
3411
  Project: [],
2713
3412
  Environment: [],
@@ -2757,6 +3456,12 @@ const commandsListCmd: CommandHandler = () => {
2757
3456
  '/install-skill': 'Tools & Skills',
2758
3457
  '/remove-skill': 'Tools & Skills',
2759
3458
  '/mcp': 'Tools & Skills',
3459
+ '/plugins': 'Plugins',
3460
+ '/browse-plugins': 'Plugins',
3461
+ '/install-plugin': 'Plugins',
3462
+ '/remove-plugin': 'Plugins',
3463
+ '/plugin-enable': 'Plugins',
3464
+ '/plugin-disable': 'Plugins',
2760
3465
  '/commands': 'Tools & Skills',
2761
3466
  '/plan': 'Workflow',
2762
3467
  '/no-plan': 'Workflow',
@@ -2786,6 +3491,18 @@ const commandsListCmd: CommandHandler = () => {
2786
3491
  '/bg': 'Agents',
2787
3492
  '/artifact': 'Artifacts',
2788
3493
  '/schedule': 'Other',
3494
+ '/commit': 'Workflow',
3495
+ '/push': 'Workflow',
3496
+ '/pr': 'Workflow',
3497
+ '/issue': 'Workflow',
3498
+ '/simplify': 'Workflow',
3499
+ '/lint': 'Workflow',
3500
+ '/files': 'Session & Identity',
3501
+ '/stats': 'Session & Identity',
3502
+ '/summary': 'Session & Identity',
3503
+ '/cd': 'Session & Identity',
3504
+ '/hooks': 'Workflow',
3505
+ '/batch': 'Workflow',
2789
3506
  }
2790
3507
 
2791
3508
  for (const name of names) {
@@ -2853,6 +3570,12 @@ registry.set('/reload-skills', reloadSkillsCmd)
2853
3570
  registry.set('/browse-skills', browseSkillsCmd)
2854
3571
  registry.set('/install-skill', installSkillCmd)
2855
3572
  registry.set('/remove-skill', removeSkillCmd)
3573
+ registry.set('/plugins', pluginsCmd)
3574
+ registry.set('/browse-plugins', browsePluginsCmd)
3575
+ registry.set('/install-plugin', installPluginCmd)
3576
+ registry.set('/remove-plugin', removePluginCmd)
3577
+ registry.set('/plugin-enable', pluginEnableCmd)
3578
+ registry.set('/plugin-disable', pluginDisableCmd)
2856
3579
  registry.set('/commands', commandsListCmd)
2857
3580
 
2858
3581
  // Workflow
@@ -2904,6 +3627,24 @@ registry.set('/artifact', artifactBaseCmd)
2904
3627
  registry.set('/branch', branchCmd)
2905
3628
  registry.set('/schedule', scheduleCmd)
2906
3629
 
3630
+ // GitHub & Git Workflow (Claude Code parity)
3631
+ registry.set('/commit', commitCmd)
3632
+ registry.set('/push', pushCmd)
3633
+ registry.set('/pr', prCmd)
3634
+ registry.set('/issue', issueCmd)
3635
+
3636
+ // Code Quality (Claude Code parity)
3637
+ registry.set('/simplify', simplifyCmd)
3638
+ registry.set('/lint', lintCmd)
3639
+
3640
+ // Session Enhancement (Claude Code parity)
3641
+ registry.set('/files', filesCmd)
3642
+ registry.set('/stats', statsCmd)
3643
+ registry.set('/summary', summaryCmd)
3644
+ registry.set('/cd', cdCmd)
3645
+ registry.set('/hooks', hooksCmd)
3646
+ registry.set('/batch', batchCmd)
3647
+
2907
3648
  // ═══════════════════════════════════════════════════════════════
2908
3649
  // Public API
2909
3650
  // ═══════════════════════════════════════════════════════════════
@@ -2961,6 +3702,12 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
2961
3702
  '/install-skill': 'Install a skill by name or URL',
2962
3703
  '/remove-skill': 'Remove an installed skill',
2963
3704
  '/mcp': 'MCP server status',
3705
+ '/plugins': 'List installed plugins',
3706
+ '/browse-plugins': 'Browse community plugin marketplace',
3707
+ '/install-plugin': 'Install a plugin from npm or local path',
3708
+ '/remove-plugin': 'Remove an installed plugin',
3709
+ '/plugin-enable': 'Enable a disabled plugin',
3710
+ '/plugin-disable': 'Disable an enabled plugin',
2964
3711
  '/plan': 'Enter plan mode',
2965
3712
  '/no-plan': 'Exit plan mode',
2966
3713
  '/tdd': 'TDD mode',
@@ -2990,6 +3737,18 @@ const COMMAND_DESCRIPTIONS: Record<string, string> = {
2990
3737
  '/bg': 'Run a background agent task',
2991
3738
  '/artifact': 'Manage artifacts',
2992
3739
  '/schedule': 'View scheduled tasks',
3740
+ '/commit': 'Generate commit message and review staged changes',
3741
+ '/push': 'Push the current branch',
3742
+ '/pr': 'Create a pull request',
3743
+ '/issue': 'File a GitHub issue',
3744
+ '/simplify': 'Review code for reuse, simplification, and quality',
3745
+ '/lint': 'Run linting on the project',
3746
+ '/files': 'List files in current working directory',
3747
+ '/stats': 'Show session usage statistics',
3748
+ '/summary': 'Generate session summary',
3749
+ '/cd': 'Change session working directory',
3750
+ '/hooks': 'Manage lifecycle hook scripts',
3751
+ '/batch': 'Apply changes across multiple files',
2993
3752
  }
2994
3753
 
2995
3754
  export function getCommandList(): CommandEntry[] {