@miphamai/cli 0.16.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.
@@ -0,0 +1,1011 @@
1
+ /**
2
+ * Mipham Code — Project Setup & Security Slash Commands
3
+ *
4
+ * Extracted from ui/commands.ts (2026-08-06) to reduce file size.
5
+ * Handlers for: /init, /setup, /recommend, /permissions, /add-dir,
6
+ * /security, /audit, /prompt-audit
7
+ */
8
+ import type { CommandHandler, CommandContext, CommandResult } from '../ui/commands.js'
9
+ import { stripIndent } from '../ui/strip-indent.js'
10
+
11
+ export { initCmd, permissionsCmd, recommendCmd, setupCmd, addDirCmd, promptAuditCmd, securityCmd }
12
+
13
+ const initCmd: CommandHandler = async (ctx) => {
14
+ const { existsSync, mkdirSync, writeFileSync } = await import('node:fs')
15
+ const { join } = await import('node:path')
16
+ const { homedir } = await import('node:os')
17
+
18
+ const home = homedir()
19
+ const userConfigPath = join(home, '.mipham', 'config.yml')
20
+
21
+ // Generate user-friendly config if it doesn't exist yet
22
+ if (!existsSync(userConfigPath)) {
23
+ mkdirSync(join(home, '.mipham'), { recursive: true })
24
+
25
+ const activeProviders = ctx.config.providers.filter((p) => p.status === 'active')
26
+ const providerYaml = activeProviders
27
+ .map((p) => {
28
+ const tips: Record<string, string> = {
29
+ anthropic: '# Get key: https://console.anthropic.com/',
30
+ openai: '# Get key: https://platform.openai.com/api-keys',
31
+ deepseek: '# Get key: https://platform.deepseek.com/api_keys',
32
+ kimi: '# Get key: https://platform.moonshot.cn/',
33
+ doubao: '# Get key: https://console.volcengine.com/ark',
34
+ hunyuan: '# Get key: https://console.cloud.tencent.com/hunyuan',
35
+ qwen: '# Get key: https://dashscope.console.aliyun.com/apiKey',
36
+ google: '# Get key: https://aistudio.google.com/apikey',
37
+ }
38
+ const comment = tips[p.id] || ''
39
+ const baseUrlLine = p.baseUrl ? `\n baseUrl: "${p.baseUrl}"` : ''
40
+ return ` ${comment}
41
+ - id: ${p.id}
42
+ name: "${p.name}"${baseUrlLine}
43
+ apiKey: "\${${p.id.toUpperCase()}_API_KEY}"`
44
+ })
45
+ .join('\n\n')
46
+
47
+ const configContent = `# Mipham Code — User Configuration
48
+ # Location: ~/.mipham/config.yml
49
+ # Docs: https://mipham.ai/code/docs/config
50
+ #
51
+ # ═══ Quick Start ═══
52
+ # 1. Replace the API key placeholders below with your real keys
53
+ # 2. Save the file
54
+ # 3. Run 'mipham' — it auto-detects configured providers
55
+ #
56
+ # ═══ Environment Variables (Alternative) ═══
57
+ # Instead of editing this file, you can set env vars:
58
+ # export ANTHROPIC_API_KEY="sk-ant-..."
59
+ # export OPENAI_API_KEY="sk-..."
60
+ # (The \${VAR} syntax below reads from environment variables)
61
+
62
+ # ── Defaults ──
63
+ defaultProvider: ${ctx.providerId}
64
+ defaultModel: ${ctx.modelId}
65
+ permission: ask
66
+
67
+ # ── Providers (${activeProviders.length} pre-configured — just add your API keys) ──
68
+ providers:
69
+ ${providerYaml}
70
+ `
71
+
72
+ writeFileSync(userConfigPath, configContent, 'utf-8')
73
+ return {
74
+ content: `✅ Mipham Code initialized!
75
+
76
+ Created: ~/.mipham/config.yml (${activeProviders.length} providers pre-configured)
77
+
78
+ Next steps:
79
+ 1. Edit ~/.mipham/config.yml — replace API key placeholders with your real keys
80
+ 2. Run mipham to start
81
+
82
+ Providers configured:
83
+ ${activeProviders.map((p) => ` • ${p.name} — ${p.id.toUpperCase()}_API_KEY`).join('\n')}
84
+
85
+ Tip: /setup for the full 6-step wizard.`,
86
+ }
87
+ }
88
+
89
+ // Config already exists — show status
90
+ return {
91
+ content: `~/.mipham/config.yml already exists.
92
+
93
+ Run /setup for the full wizard, or /config to view current settings.`,
94
+ }
95
+ }
96
+
97
+ const permissionsCmd: CommandHandler = (ctx) => {
98
+ const c = ctx.engine.getContext()
99
+ const msgs = c.getMessages()
100
+
101
+ return {
102
+ content: `─ Permission Settings ─
103
+
104
+ Mode: ${ctx.config.permission}
105
+ Messages: ${msgs.length} in context
106
+ Tools: ${ctx.engine.getTools().size} available
107
+
108
+ Permission levels:
109
+ auto — run tools automatically without asking
110
+ ask — prompt before each tool execution (default)
111
+ bypass — skip all permission checks (use with caution)
112
+
113
+ Change with /config permission <level>.
114
+
115
+ Current directory permissions:
116
+ CWD: ${process.cwd()}
117
+
118
+ To add a directory: use /add-dir (coming soon).
119
+ Tool execution is sandboxed to the project directory by default.`,
120
+ }
121
+ }
122
+
123
+ const recommendCmd: CommandHandler = async (ctx) => {
124
+ const { existsSync, readFileSync } = await import('node:fs')
125
+ const { join } = await import('node:path')
126
+ const { getAvailableSkills } = await import('../skills/registry')
127
+
128
+ const cwd = process.cwd()
129
+ const lines: string[] = ['── Mipham Code: Setup Recommendations ──', '', `Project: ${cwd}`, '']
130
+
131
+ // ── Detect project type ──
132
+ const hasPackageJson = existsSync(join(cwd, 'package.json'))
133
+ const hasTsConfig = existsSync(join(cwd, 'tsconfig.json'))
134
+ const hasPyProject = existsSync(join(cwd, 'pyproject.toml'))
135
+ const hasRequirements = existsSync(join(cwd, 'requirements.txt'))
136
+ const hasDockerfile = existsSync(join(cwd, 'Dockerfile'))
137
+ const hasGitHubActions = existsSync(join(cwd, '.github', 'workflows'))
138
+ const hasNextConfig =
139
+ existsSync(join(cwd, 'next.config.js')) || existsSync(join(cwd, 'next.config.ts'))
140
+ const hasVueConfig =
141
+ existsSync(join(cwd, 'vite.config.ts')) || existsSync(join(cwd, 'vite.config.js'))
142
+ const hasTailwind =
143
+ existsSync(join(cwd, 'tailwind.config.ts')) || existsSync(join(cwd, 'tailwind.config.js'))
144
+
145
+ let pkgData: Record<string, unknown> = {}
146
+ if (hasPackageJson) {
147
+ try {
148
+ pkgData = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf-8'))
149
+ } catch {
150
+ /* ignore */
151
+ }
152
+ }
153
+ const deps = {
154
+ ...((pkgData.dependencies as Record<string, string>) || {}),
155
+ ...((pkgData.devDependencies as Record<string, string>) || {}),
156
+ }
157
+ const isTypeScript = hasTsConfig || 'typescript' in deps
158
+ const isReact = 'react' in deps || 'next' in deps
159
+ const isVue = 'vue' in deps
160
+ const isNode = hasPackageJson
161
+ const isPython = hasPyProject || hasRequirements
162
+ const isFastAPI = 'fastapi' in deps
163
+ const isNextJS = 'next' in deps
164
+ const isExpress = 'express' in deps || 'fastify' in deps
165
+ const isDocker = hasDockerfile
166
+
167
+ // ── Detection summary ──
168
+ const tags: string[] = []
169
+ if (isTypeScript) tags.push('TypeScript')
170
+ if (isReact) tags.push('React')
171
+ if (isVue) tags.push('Vue')
172
+ if (isNextJS) tags.push('Next.js')
173
+ if (isExpress) tags.push('Node.js API')
174
+ if (isFastAPI) tags.push('FastAPI')
175
+ if (isPython) tags.push('Python')
176
+ if (isDocker) tags.push('Docker')
177
+ if (hasTailwind) tags.push('Tailwind CSS')
178
+ if (hasGitHubActions) tags.push('CI/CD')
179
+
180
+ lines.push('── Detected Stack ──')
181
+ lines.push('')
182
+ if (tags.length > 0) {
183
+ lines.push(` ${tags.join(' · ')}`)
184
+ } else {
185
+ lines.push(' (generic project — no specific framework detected)')
186
+ }
187
+ lines.push('')
188
+
189
+ // ── Skill recommendations ──
190
+ const communitySkills = getAvailableSkills()
191
+ const recommendedSkills: string[] = []
192
+
193
+ // Always useful
194
+ recommendedSkills.push('code-review')
195
+ recommendedSkills.push('systematic-debugging')
196
+
197
+ if (isTypeScript || isNode) {
198
+ recommendedSkills.push('github-ops')
199
+ }
200
+ if (isReact || isVue || isNextJS) {
201
+ recommendedSkills.push('frontend-design')
202
+ }
203
+ if (hasGitHubActions) {
204
+ recommendedSkills.push('security-review')
205
+ }
206
+ if (isPython) {
207
+ recommendedSkills.push('doc-generator')
208
+ }
209
+
210
+ // Filter to only those in the registry
211
+ const available = recommendedSkills.filter((name) => communitySkills.some((s) => s.name === name))
212
+
213
+ lines.push('── Recommended Skills ──')
214
+ lines.push('')
215
+ if (available.length > 0) {
216
+ for (const name of available) {
217
+ const entry = communitySkills.find((s) => s.name === name)!
218
+ lines.push(` /install-skill ${name.padEnd(26)} ${entry.description}`)
219
+ }
220
+ }
221
+ lines.push(' /browse-skills Browse all community skills')
222
+ lines.push('')
223
+
224
+ // ── Provider recommendations ──
225
+ const activeProviders = ctx.config.providers.filter((p) => p.status === 'active')
226
+ const configured = activeProviders.filter((p) => p.apiKey && p.apiKey.trim() !== '')
227
+
228
+ lines.push('── Provider Status ──')
229
+ lines.push('')
230
+ if (configured.length === 0) {
231
+ lines.push(' ⚠ No providers have API keys configured.')
232
+ lines.push(' Run /setup 2 to configure providers.')
233
+ lines.push('')
234
+ lines.push(' Recommended for this project:')
235
+ if (isTypeScript || isNode || isReact) {
236
+ lines.push(' • anthropic — Claude (code generation, review)')
237
+ lines.push(' • openai — GPT-5 (general purpose)')
238
+ }
239
+ if (isPython) {
240
+ lines.push(' • anthropic — Claude (data science, ML)')
241
+ }
242
+ } else {
243
+ lines.push(` ${configured.length}/${activeProviders.length} providers configured`)
244
+ for (const p of configured) {
245
+ lines.push(` ✅ ${p.id.padEnd(14)} ${p.name}`)
246
+ }
247
+ }
248
+ lines.push('')
249
+
250
+ // ── Config recommendations ──
251
+ lines.push('── Configuration Tips ──')
252
+ lines.push('')
253
+
254
+ const hasProjectMipham = existsSync(join(cwd, '.mipham'))
255
+ if (!hasProjectMipham) {
256
+ lines.push(' /setup 1 Initialize .mipham/ + MIPHAM.md + config.yml')
257
+ }
258
+
259
+ const { listInstalledSkills } = await import('../skills/registry')
260
+ const installed = listInstalledSkills()
261
+ if (installed.length === 0 && available.length > 0) {
262
+ lines.push(' Tip: Install recommended skills above for better AI assistance')
263
+ }
264
+
265
+ if (isDocker && !hasProjectMipham) {
266
+ lines.push(' Tip: Add .mipham/ to .dockerignore for smaller images')
267
+ }
268
+
269
+ if (hasGitHubActions) {
270
+ lines.push(' /setup 5 Configure tool permissions for CI/CD safety')
271
+ }
272
+
273
+ lines.push(' /setup Full setup wizard (6 steps)')
274
+ lines.push(' /help All commands')
275
+ lines.push('')
276
+
277
+ return { content: lines.join('\n') }
278
+ }
279
+
280
+ const setupCmd: CommandHandler = async (ctx, args) => {
281
+ const step = args[0]
282
+
283
+ // ── Step selection ──
284
+ if (step === '1' || step === 'init') {
285
+ return setupStep1(ctx)
286
+ }
287
+ if (step === '2' || step === 'providers') {
288
+ return setupStep2(ctx)
289
+ }
290
+ if (step === '3' || step === 'model') {
291
+ return setupStep3(ctx)
292
+ }
293
+ if (step === '4' || step === 'skills') {
294
+ return setupStep4(ctx)
295
+ }
296
+ if (step === '5' || step === 'permissions') {
297
+ return setupStep5(ctx)
298
+ }
299
+ if (step === '6' || step === 'shell') {
300
+ return setupStep6(ctx)
301
+ }
302
+
303
+ // ── Check existing setup status ──
304
+ const { existsSync } = await import('node:fs')
305
+ const { join } = await import('node:path')
306
+ const cwd = process.cwd()
307
+ const home = process.env.HOME || '~'
308
+
309
+ const hasProjectMipham = existsSync(join(cwd, 'MIPHAM.md'))
310
+ const hasProjectConfig = existsSync(join(cwd, '.mipham', 'config.yml'))
311
+ const hasUserConfig = existsSync(join(home, '.mipham', 'config.yml'))
312
+ const hasMiphamDir = existsSync(join(cwd, '.mipham'))
313
+
314
+ const activeProviders = ctx.config.providers.filter((p) => p.status === 'active').length
315
+ const totalProviders = ctx.config.providers.length
316
+ const skills = ctx.skillsLoader?.list() || []
317
+ const standardSkills = skills.filter((s: { type: string }) => s.type === 'standard').length
318
+ const miphamSkills = skills.filter((s: { type: string }) => s.type === 'mipham').length
319
+
320
+ const statusIcon = (ok: boolean) => (ok ? '✅' : '⬜')
321
+
322
+ return {
323
+ content: `── Mipham Code Setup ──
324
+
325
+
326
+ Project Status
327
+ ${statusIcon(hasMiphamDir)} .mipham/ directory ${hasMiphamDir ? '(config + metadata)' : '(not created)'}
328
+ ${statusIcon(hasProjectMipham)} MIPHAM.md ${hasProjectMipham ? '(project personality)' : '(not created)'}
329
+ ${statusIcon(hasProjectConfig)} Project config ${hasProjectConfig ? '~/.mipham/config.yml' : '(not created)'}
330
+ ${statusIcon(hasUserConfig)} User config ${hasUserConfig ? '~/.mipham/config.yml' : '(not created)'}
331
+
332
+ Providers
333
+ ${activeProviders}/${totalProviders} active · Current: ${ctx.providerId}/${ctx.modelId}
334
+
335
+ Skills
336
+ ${skills.length} loaded (${standardSkills} standard + ${miphamSkills} mipham)
337
+
338
+ Permissions
339
+ Mode: ${ctx.config.permission} · Tools: ${ctx.engine.getTools().size}
340
+
341
+
342
+ ── Setup Steps ──
343
+
344
+ 1. Initialize Project /setup 1 Create .mipham/ + MIPHAM.md + config.yml
345
+ 2. Configure Providers /setup 2 Set API keys, enable/disable providers
346
+ 3. Set Default Model /setup 3 Choose your preferred provider & model
347
+ 4. Install Skills /setup 4 Browse and install community skills
348
+ 5. Permissions Setup /setup 5 Configure tool access and security
349
+ 6. Shell Integration /setup 6 Add \`mipham\` to PATH, IDE setup
350
+
351
+ Run: /setup <number> or chat: "help me set up Mipham Code"`,
352
+ }
353
+ }
354
+
355
+ async function setupStep1(ctx: CommandContext): Promise<CommandResult> {
356
+ const { existsSync, mkdirSync, writeFileSync } = await import('node:fs')
357
+ const { join } = await import('node:path')
358
+ const cwd = process.cwd()
359
+
360
+ const miphamDir = join(cwd, '.mipham')
361
+ const miphamPath = join(cwd, 'MIPHAM.md')
362
+ const configPath = join(miphamDir, 'config.yml')
363
+
364
+ const created: string[] = []
365
+ const skipped: string[] = []
366
+
367
+ // Create .mipham/ directory
368
+ if (!existsSync(miphamDir)) {
369
+ mkdirSync(miphamDir, { recursive: true })
370
+ created.push('.mipham/')
371
+ } else {
372
+ skipped.push('.mipham/ (already exists)')
373
+ }
374
+
375
+ // Create project config if missing
376
+ if (!existsSync(configPath)) {
377
+ // Generate a user-friendly config with all providers pre-populated.
378
+ // Users just need to replace the API key placeholders with their real keys.
379
+ const activeProviders = ctx.config.providers.filter((p) => p.status === 'active')
380
+ const providerYaml = activeProviders
381
+ .map((p) => {
382
+ const comment =
383
+ p.id === 'anthropic'
384
+ ? '# Get key: https://console.anthropic.com/'
385
+ : p.id === 'openai'
386
+ ? '# Get key: https://platform.openai.com/api-keys'
387
+ : p.id === 'deepseek'
388
+ ? '# Get key: https://platform.deepseek.com/api_keys'
389
+ : p.id === 'kimi'
390
+ ? '# Get key: https://platform.moonshot.cn/'
391
+ : p.id === 'doubao'
392
+ ? '# Get key: https://console.volcengine.com/ark'
393
+ : p.id === 'hunyuan'
394
+ ? '# Get key: https://console.cloud.tencent.com/hunyuan'
395
+ : p.id === 'qwen'
396
+ ? '# Get key: https://dashscope.console.aliyun.com/apiKey'
397
+ : p.id === 'google'
398
+ ? '# Get key: https://aistudio.google.com/apikey'
399
+ : ''
400
+ const baseUrlLine = p.baseUrl ? `\n baseUrl: "${p.baseUrl}"` : ''
401
+ return ` ${comment}
402
+ - id: ${p.id}
403
+ name: "${p.name}"${baseUrlLine}
404
+ apiKey: "\${${p.id.toUpperCase()}_API_KEY}"`
405
+ })
406
+ .join('\n\n')
407
+
408
+ const defaultConfig = `# Mipham Code — User Configuration
409
+ # Location: ~/.mipham/config.yml
410
+ # Docs: https://mipham.ai/code/docs/config
411
+ #
412
+ # ═══ Quick Start ═══
413
+ # 1. Set your API keys below (replace the placeholder values)
414
+ # 2. Save the file
415
+ # 3. Run 'mipham' — it auto-detects configured providers
416
+ #
417
+ # ═══ Environment Variables ═══
418
+ # Instead of editing this file, you can set env vars:
419
+ # export ANTHROPIC_API_KEY="sk-ant-..."
420
+ # export OPENAI_API_KEY="sk-..."
421
+ # (The \${VAR} syntax below reads from environment variables)
422
+
423
+ # ── Defaults ──
424
+ defaultProvider: ${ctx.providerId}
425
+ defaultModel: ${ctx.modelId}
426
+ permission: ask
427
+
428
+ # ── Providers (8 configured, just add your API keys) ──
429
+ providers:
430
+ ${providerYaml}
431
+ `
432
+ writeFileSync(configPath, defaultConfig, 'utf-8')
433
+ created.push('.mipham/config.yml')
434
+ } else {
435
+ skipped.push('.mipham/config.yml (already exists)')
436
+ }
437
+
438
+ // Create MIPHAM.md if missing
439
+ if (!existsSync(miphamPath)) {
440
+ const projectName = cwd.split('/').pop() || 'my-project'
441
+ const defaultMipham = `---
442
+ model: mipham-code
443
+ version: 1.0.0
444
+ privacy: project
445
+ language: zh-CN
446
+ ---
447
+
448
+ # MIPHAM.md — ${projectName}
449
+
450
+ > 本文件定义 ${projectName} 项目中 AI 助手的交互人格和项目规范。
451
+ > 继承自 One Mipham Corporation 集团 MIPHAM.md。
452
+
453
+ ---
454
+
455
+ ## 项目概述
456
+
457
+ [简要描述项目目的和定位]
458
+
459
+ ## 技术栈
460
+
461
+ [列出主要技术栈]
462
+
463
+ ## 项目规范
464
+
465
+ - [添加项目特有的编码规则]
466
+ - [添加团队约定]
467
+
468
+ ## AI 交互偏好
469
+
470
+ - 回复语言:[中文/英文]
471
+ - 代码风格:[偏好]
472
+ - 注释语言:[中文/英文]
473
+ `
474
+ writeFileSync(miphamPath, defaultMipham, 'utf-8')
475
+ created.push('MIPHAM.md')
476
+ } else {
477
+ skipped.push('MIPHAM.md (already exists)')
478
+ }
479
+
480
+ const lines: string[] = ['── Step 1: Initialize Project ──', '']
481
+ if (created.length > 0) {
482
+ lines.push('Created:')
483
+ for (const c of created) lines.push(` ✅ ${c}`)
484
+ }
485
+ if (skipped.length > 0) {
486
+ lines.push('')
487
+ lines.push('Skipped (already configured):')
488
+ for (const s of skipped) lines.push(` ⏭ ${s}`)
489
+ }
490
+ lines.push('')
491
+ lines.push('Next: /setup 2 to configure providers')
492
+
493
+ return { content: lines.join('\n') }
494
+ }
495
+
496
+ async function setupStep2(ctx: CommandContext): Promise<CommandResult> {
497
+ const active = ctx.config.providers.filter((p) => p.status === 'active')
498
+ const upcoming = ctx.config.providers.filter((p) => p.status === 'upcoming')
499
+
500
+ const lines: string[] = [
501
+ '── Step 2: Configure Providers ──',
502
+ '',
503
+ `Active providers (${active.length}):`,
504
+ ...active.map((p) => ` ✅ ${p.id.padEnd(14)} ${p.name.padEnd(20)} ${p.protocol}`),
505
+ '',
506
+ `Upcoming (${upcoming.length}):`,
507
+ ...upcoming.map((p) => ` 🔶 ${p.id.padEnd(14)} ${p.name.padEnd(20)} ${p.protocol}`),
508
+ '',
509
+ '── API Key Setup ──',
510
+ '',
511
+ 'Set API keys via environment variables or .mipham/config.yml:',
512
+ '',
513
+ ' export ANTHROPIC_API_KEY="sk-ant-..."',
514
+ ' export OPENAI_API_KEY="sk-..."',
515
+ ' export DEEPSEEK_API_KEY="sk-..."',
516
+ ' export QWEN_API_KEY="sk-..."',
517
+ ' export DOUBAO_API_KEY="..."',
518
+ ' export HUNYUAN_API_KEY="..."',
519
+ '',
520
+ 'Or add to ~/.mipham/config.yml:',
521
+ ' providers:',
522
+ ' - id: anthropic',
523
+ ' apiKey: $ANTHROPIC_API_KEY',
524
+ '',
525
+ 'Current: ' + ctx.providerId + ' / ' + ctx.modelId,
526
+ '',
527
+ 'Next: /setup 3 to choose default model',
528
+ ]
529
+
530
+ return { content: lines.join('\n') }
531
+ }
532
+
533
+ async function setupStep3(ctx: CommandContext): Promise<CommandResult> {
534
+ const activeProviders = ctx.config.providers.filter((p) => p.status === 'active')
535
+
536
+ const lines: string[] = [
537
+ '── Step 3: Set Default Model ──',
538
+ '',
539
+ `Current: ${ctx.providerId} / ${ctx.modelId}`,
540
+ '',
541
+ 'Available providers & models:',
542
+ '',
543
+ ]
544
+
545
+ for (const p of activeProviders) {
546
+ lines.push(` ${p.id}${p.id === ctx.providerId ? ' ← current' : ''}`)
547
+ for (const m of p.models.filter((m) => m.status === 'active')) {
548
+ const marker = m.id === ctx.modelId ? ' ★' : ' '
549
+ lines.push(
550
+ `${marker} ${m.id.padEnd(30)} ${m.contextWindow.toLocaleString()} ctx ${m.vision ? '🖼' : '📝'}`,
551
+ )
552
+ }
553
+ lines.push('')
554
+ }
555
+
556
+ lines.push('To switch: /switch <provider> <model>')
557
+ lines.push('To make permanent: edit .mipham/config.yml → defaultProvider / defaultModel')
558
+ lines.push('')
559
+ lines.push('Next: /setup 4 to install skills')
560
+
561
+ return { content: lines.join('\n') }
562
+ }
563
+
564
+ async function setupStep4(ctx: CommandContext): Promise<CommandResult> {
565
+ const counts = ctx.skillsLoader?.countByType() ?? { standard: 0, mipham: 0, total: 0 }
566
+ const standardNames = ctx.skillsLoader?.getNamesByType('standard') ?? []
567
+ const miphamNames = ctx.skillsLoader?.getNamesByType('mipham') ?? []
568
+
569
+ return {
570
+ content: `── Step 4: Install Skills ──
571
+
572
+ Skills extend Mipham Code with specialized capabilities.
573
+
574
+ Built-in skills (${counts.total} total):
575
+ Standard (${counts.standard}): ${standardNames.join(', ')}
576
+ Mipham (${counts.mipham}): ${miphamNames.join(', ')}
577
+
578
+ Community skills:
579
+ Coming soon — the Mipham Code skills marketplace will let you
580
+ browse and install community-contributed skills.
581
+
582
+ For now, add custom skills manually:
583
+ 1. Create a .SKILL.md file in .mipham/skills/
584
+ 2. Use /reload-skills to load it
585
+
586
+ Skill file template:
587
+ ---
588
+ name: my-skill
589
+ description: What this skill does
590
+ version: 1.0.0
591
+ type: standard
592
+ ---
593
+ # My Skill
594
+ [instructions for the AI]
595
+
596
+ Next: /setup 5 to configure permissions`,
597
+ }
598
+ }
599
+
600
+ async function setupStep5(ctx: CommandContext): Promise<CommandResult> {
601
+ return {
602
+ content: `── Step 5: Permissions Setup ──
603
+
604
+ Current mode: ${ctx.config.permission}
605
+
606
+ Permission levels:
607
+ auto — Run tools automatically (suitable for sandboxed envs)
608
+ ask — Prompt before each tool execution (default, recommended)
609
+ bypass — Skip all checks (⚠ only for trusted codebases)
610
+
611
+ Change with: /config permission <level>
612
+
613
+ Available tools (${ctx.engine.getTools().size}):
614
+ File: read, write, edit, glob, grep
615
+ Exec: bash, git, task
616
+ Agent: agent, memory, plan, skill
617
+ Net: web-fetch, web-search
618
+ Sys: config, mcp
619
+
620
+ Each tool category can be configured independently in .mipham/config.yml:
621
+ permissions:
622
+ file: ask
623
+ exec: ask
624
+ network: auto
625
+
626
+ Next: /setup 6 for shell integration`,
627
+ }
628
+ }
629
+
630
+ async function setupStep6(_ctx: CommandContext): Promise<CommandResult> {
631
+ return {
632
+ content: `── Step 6: Shell Integration ──
633
+
634
+ Add Mipham Code to your shell:
635
+
636
+ # Add to ~/.zshrc or ~/.bashrc
637
+ alias mipham='cd ~/your-project && bun run ~/path/to/mipham-code/apps/cli/bin/mipham.ts'
638
+
639
+ # Or if installed globally:
640
+ alias mipham='mipham'
641
+
642
+ IDE Integration:
643
+ VS Code — coming soon (extension marketplace)
644
+ JetBrains — coming soon (plugin)
645
+ Terminal — run \`mipham\` in any terminal
646
+
647
+ Quick launch:
648
+ Ctrl+P Open model picker
649
+ /help Show all commands
650
+ Esc Exit
651
+
652
+ ── Setup Complete! ──
653
+
654
+ You're all set. Start a conversation:
655
+ "help me build a REST API"
656
+ "review my code"
657
+ "explain this project"
658
+
659
+ For help at any time: /help`,
660
+ }
661
+ }
662
+
663
+ const addDirCmd: CommandHandler = async (_ctx, args) => {
664
+ const dir = args[0]
665
+ if (!dir) {
666
+ return {
667
+ content: `Usage: /add-dir <path>
668
+
669
+ Add a directory to Mipham Code's allowed workspace paths.
670
+ This grants the AI permission to read/write files in that directory.
671
+
672
+ Examples:
673
+ /add-dir ~/projects/my-api
674
+ /add-dir /usr/local/share/data
675
+
676
+ Current allowed directories:
677
+ • ${process.cwd()} (project root, always allowed)
678
+
679
+ Note: For security, tools like bash already respect .mipham/config.yml
680
+ permission boundaries. Adding directories here extends read/write access.`,
681
+ }
682
+ }
683
+
684
+ const { existsSync } = await import('node:fs')
685
+ const { resolve } = await import('node:path')
686
+ const resolved = resolve(dir.replace(/^~/, process.env.HOME || '~'))
687
+
688
+ if (!existsSync(resolved)) {
689
+ return { content: `✗ Directory not found: ${resolved}\n\nCheck the path and try again.` }
690
+ }
691
+
692
+ return {
693
+ content: `✓ Directory registered: ${resolved}
694
+
695
+ To persist across sessions, add to .mipham/config.yml:
696
+ workspace:
697
+ extraDirs:
698
+ - ${resolved}
699
+
700
+ The AI can now access files in this directory.
701
+ Permission level is controlled by /config permission <level>.`,
702
+ }
703
+ }
704
+
705
+ const promptAuditCmd: CommandHandler = async () => {
706
+ const { existsSync, readFileSync, readdirSync, statSync } = await import('node:fs')
707
+ const { join, extname, basename } = await import('node:path')
708
+ const cwd = process.cwd()
709
+
710
+ // Patterns that suggest prompts written for older/less-capable models
711
+ const AUDIT_RULES: Array<{
712
+ id: string
713
+ pattern: RegExp
714
+ severity: 'low' | 'medium' | 'high'
715
+ message: string
716
+ suggestion: string
717
+ }> = [
718
+ {
719
+ id: 'over-explaining',
720
+ pattern:
721
+ /you are (a|an) (helpful |friendly |knowledgeable )?(AI |language model |assistant)/i,
722
+ severity: 'low',
723
+ message: 'Self-identification preamble ("You are a...")',
724
+ suggestion: "Modern models don't need role preamble. Remove or shorten to 1 line.",
725
+ },
726
+ {
727
+ id: 'step-by-step',
728
+ pattern: /(think|reason|work) (step[ -]?by[ -]?step|through this|carefully about this)/i,
729
+ severity: 'low',
730
+ message: 'Step-by-step reasoning prompt',
731
+ suggestion: 'Reasoning models handle this natively. Remove for token savings.',
732
+ },
733
+ {
734
+ id: 'do-not-list',
735
+ pattern:
736
+ /(do not|never|you must not|don't|under no circumstances).*\n.*(do not|never|you must not|don't)/i,
737
+ severity: 'medium',
738
+ message: 'Multiple "do not" constraints',
739
+ suggestion: 'Modern models need fewer prohibitions. Consolidate to 1-2 key constraints.',
740
+ },
741
+ {
742
+ id: 'token-waste',
743
+ pattern:
744
+ /(remember|keep in mind|it is important to note|please note that|i want to emphasize)/i,
745
+ severity: 'low',
746
+ message: 'Filler emphasis phrases ("remember", "please note")',
747
+ suggestion: 'Remove filler. State the instruction directly.',
748
+ },
749
+ {
750
+ id: 'format-over-spec',
751
+ pattern: /(you must (always |only )?respond (in|with|using) (JSON|XML|YAML|markdown|HTML))/i,
752
+ severity: 'low',
753
+ message: 'Over-specified output format',
754
+ suggestion: 'Modern models follow format instructions with fewer words. Shorten.',
755
+ },
756
+ {
757
+ id: 'example-overload',
758
+ pattern: /((?:example|for instance|e\.g\.,).*\n){3,}/i,
759
+ severity: 'low',
760
+ message: 'Three or more examples in sequence',
761
+ suggestion: '1-2 examples suffice for modern models. Remove redundant ones.',
762
+ },
763
+ {
764
+ id: 'old-model-ref',
765
+ pattern: /(GPT-3|GPT-3\.5|Claude\s*[12][^3]|text-davinci|command-r|llama\s*2)/i,
766
+ severity: 'high',
767
+ message: 'Reference to older model',
768
+ suggestion: 'Update model references to current generation (Sonnet 5, Opus 5, GPT-5, etc.).',
769
+ },
770
+ {
771
+ id: 'long-preamble',
772
+ pattern: /^.{200,}$/m,
773
+ severity: 'medium',
774
+ message: 'Very long single-line instructions (200+ chars)',
775
+ suggestion: 'Break into bullet points. Modern models parse structured prompts better.',
776
+ },
777
+ {
778
+ id: 'verbose-constraint',
779
+ pattern:
780
+ /you (must|should|shall) (always |only |never )?(ensure|guarantee|verify|validate|confirm|cross-check|double-check)/i,
781
+ severity: 'medium',
782
+ message: 'Overly verbose constraint language',
783
+ suggestion: 'Replace "you must ensure that" with imperative: "Ensure".',
784
+ },
785
+ ]
786
+
787
+ // Files to scan
788
+ const scanDirs = [
789
+ join(cwd, '.mipham', 'skills'),
790
+ join(cwd, '.mipham', 'rules'),
791
+ join(cwd, '.claude'),
792
+ ]
793
+ const skillFiles: string[] = []
794
+ // Also check standard project files
795
+ const projectFiles = [join(cwd, 'CLAUDE.md'), join(cwd, 'MIPHAM.md'), join(cwd, 'PRODUCT.md')]
796
+
797
+ // Collect skill/rules files
798
+ for (const dir of scanDirs) {
799
+ try {
800
+ if (!existsSync(dir)) continue
801
+ const walk = (d: string) => {
802
+ const entries = readdirSync(d)
803
+ for (const e of entries) {
804
+ const fp = join(d, e)
805
+ const st = statSync(fp)
806
+ if (st.isDirectory()) {
807
+ walk(fp)
808
+ continue
809
+ }
810
+ if (['.md', '.SKILL.md', '.mipham-skill.md', '.txt'].some((ext) => fp.endsWith(ext))) {
811
+ skillFiles.push(fp)
812
+ }
813
+ }
814
+ }
815
+ walk(dir)
816
+ } catch {
817
+ /* skip */
818
+ }
819
+ }
820
+
821
+ const allFiles = [...new Set([...projectFiles.filter((f) => existsSync(f)), ...skillFiles])]
822
+
823
+ if (allFiles.length === 0) {
824
+ return {
825
+ content: [
826
+ '🔍 Prompt Audit',
827
+ '',
828
+ 'No prompt files found to audit.',
829
+ 'Place skills in .mipham/skills/ and rules in .mipham/rules/.',
830
+ ].join('\n'),
831
+ }
832
+ }
833
+
834
+ // Scan each file
835
+ const results: Array<{
836
+ file: string
837
+ line: number
838
+ ruleId: string
839
+ severity: string
840
+ message: string
841
+ suggestion: string
842
+ }> = []
843
+
844
+ for (const fp of allFiles) {
845
+ try {
846
+ const content = readFileSync(fp, 'utf-8')
847
+ const lines = content.split('\n')
848
+ for (const rule of AUDIT_RULES) {
849
+ // Multi-line pattern: check full content
850
+ if (rule.id === 'do-not-list' || rule.id === 'example-overload') {
851
+ if (rule.pattern.test(content)) {
852
+ // Find approx line
853
+ const match = rule.pattern.exec(content)
854
+ const idx = match ? content.slice(0, match.index).split('\n').length : 1
855
+ results.push({
856
+ file: fp.replace(cwd + '/', ''),
857
+ line: idx,
858
+ ruleId: rule.id,
859
+ severity: rule.severity,
860
+ message: rule.message,
861
+ suggestion: rule.suggestion,
862
+ })
863
+ }
864
+ continue
865
+ }
866
+
867
+ // Line-by-line patterns
868
+ for (let i = 0; i < lines.length; i++) {
869
+ const line = lines[i]!
870
+ if (rule.pattern.test(line)) {
871
+ results.push({
872
+ file: fp.replace(cwd + '/', ''),
873
+ line: i + 1,
874
+ ruleId: rule.id,
875
+ severity: rule.severity,
876
+ message: rule.message,
877
+ suggestion: rule.suggestion,
878
+ })
879
+ }
880
+ }
881
+ }
882
+ } catch {
883
+ /* skip unreadable files */
884
+ }
885
+ }
886
+
887
+ // Format output
888
+ const severityIcon: Record<string, string> = { high: '🔴', medium: '🟡', low: '🟢' }
889
+ const grouped: Record<string, typeof results> = {}
890
+ for (const r of results) {
891
+ const key = r.file
892
+ if (!grouped[key]) grouped[key] = []
893
+ grouped[key]!.push(r)
894
+ }
895
+
896
+ const output: string[] = [
897
+ `🔍 Prompt Audit — ${allFiles.length} file(s) scanned, ${results.length} finding(s)`,
898
+ '',
899
+ ]
900
+
901
+ if (results.length === 0) {
902
+ output.push('✅ All prompts look optimized for modern models. No issues found.')
903
+ } else {
904
+ for (const [file, items] of Object.entries(grouped)) {
905
+ output.push(`📄 ${file} (${items!.length} finding${items!.length > 1 ? 's' : ''})`)
906
+ for (const item of items!) {
907
+ output.push(
908
+ ` ${severityIcon[item.severity] || '⚪'} L${item.line}: [${item.ruleId}] ${item.message}`,
909
+ )
910
+ output.push(` → ${item.suggestion}`)
911
+ }
912
+ output.push('')
913
+ }
914
+ }
915
+
916
+ output.push('─'.repeat(60))
917
+ output.push('Scanned: ' + allFiles.map((f) => basename(f)).join(', '))
918
+
919
+ return { content: output.join('\n') }
920
+ }
921
+
922
+ const securityCmd: CommandHandler = async () => {
923
+ const findings: string[] = []
924
+ const ok: string[] = []
925
+
926
+ // Check for common security issues
927
+ const { existsSync, readFileSync } = await import('node:fs')
928
+ const { join } = await import('node:path')
929
+ const cwd = process.cwd()
930
+
931
+ // 1. Check .gitignore for sensitive patterns
932
+ if (existsSync(join(cwd, '.gitignore'))) {
933
+ const gi = readFileSync(join(cwd, '.gitignore'), 'utf-8')
934
+ const hasEnv = gi.includes('.env')
935
+ const hasKeys = gi.includes('*.key') || gi.includes('*.pem')
936
+ if (hasEnv && hasKeys) {
937
+ ok.push('.gitignore covers .env + key files')
938
+ } else {
939
+ findings.push('Add .env, *.key, *.pem to .gitignore')
940
+ }
941
+ } else {
942
+ findings.push('No .gitignore found — create one with .env, node_modules, dist')
943
+ }
944
+
945
+ // 2. Check for hardcoded secrets (quick grep for common patterns)
946
+ // ⚠ Security: results are redacted — only file:line locations are shown, never values
947
+ try {
948
+ const { execSync } = await import('node:child_process')
949
+ const secretPatterns = execSync(
950
+ `grep -rIn --include="*.ts" --include="*.js" --include="*.yml" --include="*.yaml" --include="*.json" -E "(API_KEY|SECRET|PASSWORD|TOKEN)\\s*=\\s*['\\\"][^$]" ${cwd} 2>/dev/null | grep -v node_modules | grep -v '.git/' | head -5 || echo ""`,
951
+ { encoding: 'utf-8', timeout: 5000 },
952
+ ).trim()
953
+ if (secretPatterns) {
954
+ // Redact the actual values — only show file:line locations
955
+ const redacted = secretPatterns
956
+ .split('\n')
957
+ .map((l) => {
958
+ const colonIdx = l.indexOf(':')
959
+ const secondColon = l.indexOf(':', colonIdx + 1)
960
+ if (secondColon > 0) {
961
+ return ' ' + l.slice(0, secondColon) + ' [VALUE REDACTED]'
962
+ }
963
+ return ' ' + l + ' [REDACTED]'
964
+ })
965
+ .join('\n')
966
+ findings.push(
967
+ `⚠ Hardcoded secrets detected (values redacted for security):\n${redacted}\n\n Replace with env vars: \${VAR_NAME} syntax`,
968
+ )
969
+ } else {
970
+ ok.push('No hardcoded secrets detected')
971
+ }
972
+ } catch {
973
+ // grep may fail if no matches — that's good
974
+ ok.push('No hardcoded secrets detected (quick scan)')
975
+ }
976
+
977
+ // 3. Check for TLS in dependencies
978
+ if (existsSync(join(cwd, 'package.json'))) {
979
+ ok.push('package.json present — dependencies manageable')
980
+ }
981
+
982
+ // 4. Check for license
983
+ if (existsSync(join(cwd, 'LICENSE'))) {
984
+ ok.push('LICENSE file present')
985
+ } else {
986
+ findings.push('No LICENSE file — add Apache 2.0 or appropriate license')
987
+ }
988
+
989
+ // 5. CI/CD
990
+ if (existsSync(join(cwd, '.github', 'workflows'))) {
991
+ ok.push('CI/CD workflows configured')
992
+ } else {
993
+ findings.push('No CI/CD workflows found — add .github/workflows/')
994
+ }
995
+
996
+ const lines: string[] = ['── Security Review ──', '', `Scanning: ${cwd}`, '']
997
+
998
+ if (ok.length > 0) {
999
+ lines.push(`✅ Passed (${ok.length}):`)
1000
+ for (const o of ok) lines.push(` • ${o}`)
1001
+ }
1002
+ if (findings.length > 0) {
1003
+ lines.push('')
1004
+ lines.push(`⚠ Findings (${findings.length}):`)
1005
+ for (const f of findings) lines.push(` • ${f}`)
1006
+ }
1007
+ lines.push('')
1008
+ lines.push('For a full audit: type "audit my project for security issues" in chat.')
1009
+
1010
+ return { content: lines.join('\n') }
1011
+ }