@miphamai/cli 0.2.3

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.
Files changed (67) hide show
  1. package/README.md +76 -0
  2. package/assets/icon.icns +0 -0
  3. package/assets/icon.jpg +0 -0
  4. package/bin/mipham +51 -0
  5. package/bin/mipham.ts +8 -0
  6. package/package.json +59 -0
  7. package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
  8. package/skills/mipham/om-security.mipham-skill.md +21 -0
  9. package/skills/standard/code-review.SKILL.md +116 -0
  10. package/skills/standard/compassionate-communication.SKILL.md +80 -0
  11. package/skills/standard/doc-generator.SKILL.md +21 -0
  12. package/skills/standard/github-ops.SKILL.md +22 -0
  13. package/skills/standard/memory.SKILL.md +22 -0
  14. package/skills/standard/mipham-code-setup.SKILL.md +113 -0
  15. package/skills/standard/security-review.SKILL.md +122 -0
  16. package/skills/standard/self-review.SKILL.md +20 -0
  17. package/skills/standard/superpower.SKILL.md +19 -0
  18. package/skills/standard/tdd.SKILL.md +20 -0
  19. package/skills/standard/web-search.SKILL.md +23 -0
  20. package/src/agent/sub-agent.ts +122 -0
  21. package/src/config/defaults.ts +10 -0
  22. package/src/config/loader.ts +30 -0
  23. package/src/core/context.ts +135 -0
  24. package/src/core/engine.ts +193 -0
  25. package/src/core/hooks.ts +116 -0
  26. package/src/core/instructions.ts +126 -0
  27. package/src/core/permission.ts +54 -0
  28. package/src/core/session-store.ts +136 -0
  29. package/src/index.tsx +93 -0
  30. package/src/mcp/client.ts +170 -0
  31. package/src/mcp/protocol.ts +104 -0
  32. package/src/mcp/transport.ts +169 -0
  33. package/src/mcp/types.ts +112 -0
  34. package/src/providers/anthropic.ts +286 -0
  35. package/src/providers/bootstrap.ts +28 -0
  36. package/src/providers/openai-compat.ts +158 -0
  37. package/src/providers/registry.ts +70 -0
  38. package/src/security/path.ts +90 -0
  39. package/src/security/url.ts +96 -0
  40. package/src/shared/constants.ts +368 -0
  41. package/src/shared/index.ts +2 -0
  42. package/src/shared/types.ts +161 -0
  43. package/src/skills/loader.ts +109 -0
  44. package/src/skills/mipham/runtime.ts +63 -0
  45. package/src/skills/standard/runtime.ts +59 -0
  46. package/src/tools/agent/agent.ts +51 -0
  47. package/src/tools/agent/memory.ts +51 -0
  48. package/src/tools/agent/plan.ts +87 -0
  49. package/src/tools/agent/skill.ts +58 -0
  50. package/src/tools/exec/bash.ts +111 -0
  51. package/src/tools/exec/git.ts +63 -0
  52. package/src/tools/exec/task.ts +69 -0
  53. package/src/tools/file/edit.ts +57 -0
  54. package/src/tools/file/glob.ts +29 -0
  55. package/src/tools/file/grep.ts +52 -0
  56. package/src/tools/file/read.ts +38 -0
  57. package/src/tools/file/write.ts +27 -0
  58. package/src/tools/index.ts +126 -0
  59. package/src/tools/network/web-fetch.ts +61 -0
  60. package/src/tools/network/web-search.ts +32 -0
  61. package/src/tools/system/config.ts +68 -0
  62. package/src/tools/system/mcp.ts +75 -0
  63. package/src/ui/app.tsx +317 -0
  64. package/src/ui/chat.tsx +76 -0
  65. package/src/ui/commands.ts +2232 -0
  66. package/src/ui/input.tsx +83 -0
  67. package/src/ui/picker.tsx +209 -0
@@ -0,0 +1,2232 @@
1
+ /**
2
+ * Mipham Code — Slash Command Registry
3
+ *
4
+ * All commands mirror Claude Code's UX so users need zero re-learning.
5
+ * Commands marked [stub] are recognized but indicate WIP status.
6
+ */
7
+ import type { QueryEngine } from '../core/engine'
8
+ import type { MiphamConfig } from '../shared/index.ts'
9
+ import type { SkillsLoader } from '../skills/loader'
10
+
11
+ export interface CommandContext {
12
+ engine: QueryEngine
13
+ config: MiphamConfig
14
+ providerId: string
15
+ modelId: string
16
+ version: string
17
+ // Callbacks for commands that mutate App state
18
+ setSessionTitle: (title: string) => void
19
+ setFastMode: (on: boolean) => void
20
+ setEffort: (level: string) => void
21
+ setFocusMode: (on: boolean) => void
22
+ setGoal: (text: string) => void
23
+ skillsLoader?: SkillsLoader
24
+ }
25
+
26
+ export interface CommandResult {
27
+ /** System message to display */
28
+ content: string
29
+ /** If true, exit the app */
30
+ exit?: boolean
31
+ /** If provided, update provider after command */
32
+ nextProvider?: string
33
+ /** If provided, update model after command */
34
+ nextModel?: string
35
+ /** If true, clear all messages (handled by caller) */
36
+ clearMessages?: boolean
37
+ /** Content to copy to clipboard (handled by caller) */
38
+ copyContent?: string
39
+ }
40
+
41
+ type CommandHandler = (
42
+ ctx: CommandContext,
43
+ args: string[],
44
+ ) => CommandResult | Promise<CommandResult>
45
+
46
+ // ═══════════════════════════════════════════════════════════════
47
+ // Session & Identity
48
+ // ═══════════════════════════════════════════════════════════════
49
+
50
+ const helpCmd: CommandHandler = (_ctx) => ({
51
+ content: stripIndent`
52
+ Mipham Code v0.2.0 — Commands
53
+
54
+ ── Session ──────────────────────────
55
+ /help Show this help
56
+ /version Show version info
57
+ /clear Clear conversation
58
+ /compact Compact context window
59
+ /context Show context stats
60
+ /status Session and system status
61
+ /cost Token usage estimate
62
+ /usage Detailed usage dashboard
63
+ /rename <name> Rename current session
64
+ /goal <text> Set session goal
65
+ /recap Summarize session so far
66
+ /export Export conversation to file
67
+ /doctor System diagnostics
68
+ /resume List saved sessions
69
+ /branch <name> Fork conversation
70
+
71
+ ── History ─────────────────────────
72
+ /rewind Undo last AI turn
73
+ /undo Same as /rewind
74
+ /copy [N] Copy last response to clipboard
75
+ /focus Toggle focus view (last exchange only)
76
+
77
+ ── Model & Provider ────────────────
78
+ /pick Open model picker (or Ctrl+P)
79
+ /model Show current model
80
+ /models List all available models
81
+ /provider Show current provider
82
+ /providers List configured providers
83
+ /switch <p> <m> Switch provider and model
84
+ /config View configuration
85
+ /fast [on|off] Toggle fast mode
86
+ /effort <lvl> Set reasoning effort (low|medium|high|xhigh|max)
87
+ /theme [dark|light|auto] Set terminal theme
88
+
89
+ ── Tools & Skills ──────────────────
90
+ /tools List available tools (16 total)
91
+ /skills List loaded skills (14 built-in)
92
+ /reload-skills Reload all skills
93
+ /mcp MCP server status
94
+
95
+ ── Workflow ────────────────────────
96
+ /plan Enter plan mode (read-only)
97
+ /no-plan Exit plan mode
98
+ /tdd TDD mode [stub]
99
+ /todos Task management
100
+ /tasks Background tasks
101
+ /review Code review workflow
102
+ /pr-comments PR review summary
103
+ /diff Show git diff
104
+ /workflows List workflow scripts
105
+ /loop <int> <p> Run prompt on interval
106
+ /schedule View scheduled tasks [stub]
107
+
108
+ ── Project ─────────────────────────
109
+ /init Initialize .mipham config
110
+ /setup Guided project setup wizard
111
+ /permissions Show permission settings
112
+ /add-dir <dir> Add workspace directory
113
+ /security Security review checklist
114
+ /audit Same as /security
115
+
116
+ ── Environment ─────────────────────
117
+ /upgrade Show upgrade instructions
118
+ /release-notes View version changelog
119
+ /ide IDE integration guide
120
+ /terminal-setup Shell & terminal config
121
+ /memory Manage AI memories
122
+
123
+ ── Account ─────────────────────────
124
+ /login Show API key status
125
+ /logout Clear credentials guide
126
+ /feedback Send feedback
127
+ /agents Agent management
128
+
129
+ Type /exit or Esc to quit.
130
+ `,
131
+ })
132
+
133
+ const versionCmd: CommandHandler = (ctx) => ({
134
+ content: stripIndent`
135
+ Mipham Code v${ctx.version}
136
+
137
+ Runtime: Bun ${typeof Bun !== 'undefined' ? Bun.version : '(Node.js)'}
138
+ Platform: ${process.platform} ${process.arch}
139
+ Node: ${process.version}
140
+ CWD: ${process.cwd()}
141
+
142
+ Provider: ${ctx.providerId} / ${ctx.modelId}
143
+ Tools: 16 built-in
144
+ Skills: 14 built-in (12 standard + 2 mipham)
145
+ License: Apache 2.0
146
+ `,
147
+ })
148
+
149
+ const clearCmd: CommandHandler = (ctx) => {
150
+ ctx.engine.getContext().clear()
151
+ return { content: '✓ Conversation cleared. Context reset.', clearMessages: true }
152
+ }
153
+
154
+ const exitCmd: CommandHandler = () => ({ content: '', exit: true })
155
+
156
+ // ═══════════════════════════════════════════════════════════════
157
+ // Context & Status
158
+ // ═══════════════════════════════════════════════════════════════
159
+
160
+ const compactCmd: CommandHandler = async (ctx) => {
161
+ const context = ctx.engine.getContext()
162
+ const before = context.getEstimatedTokens()
163
+ await context.compact('user requested compaction')
164
+ const after = context.getEstimatedTokens()
165
+ return {
166
+ content: `✓ Context compacted.\nTokens: ${before.toLocaleString()} → ${after.toLocaleString()} (saved ${((1 - after / before) * 100).toFixed(0)}%)`,
167
+ }
168
+ }
169
+
170
+ const contextCmd: CommandHandler = (ctx) => {
171
+ const c = ctx.engine.getContext()
172
+ const tokens = c.getEstimatedTokens()
173
+ const msgs = c.getMessages()
174
+ const systemPromptLen = c.getSystemPrompt().length
175
+ return {
176
+ content: stripIndent`
177
+ ── Context Stats ──
178
+ Messages: ${msgs.length}
179
+ Estimated tokens: ${tokens.toLocaleString()} / 200,000
180
+ Usage: ${((tokens / 200_000) * 100).toFixed(1)}%
181
+ System prompt: ${systemPromptLen.toLocaleString()} chars (~${Math.ceil(systemPromptLen / 4).toLocaleString()} tokens)
182
+ Compaction: at 90% (${(200_000 * 0.9).toLocaleString()} tokens)
183
+ `,
184
+ }
185
+ }
186
+
187
+ const statusCmd: CommandHandler = (ctx) => {
188
+ const c = ctx.engine.getContext()
189
+ const tools = ctx.engine.getTools()
190
+ return {
191
+ content: stripIndent`
192
+ ── Session Status ──
193
+ Provider: ${ctx.providerId}
194
+ Model: ${ctx.modelId}
195
+ Messages: ${c.getMessages().length}
196
+ Tokens: ~${c.getEstimatedTokens().toLocaleString()} / 200,000
197
+ Tools: ${tools.size} loaded
198
+ Permission: ${ctx.config.permission}
199
+
200
+ ── System ──
201
+ Platform: ${process.platform} ${process.arch}
202
+ Runtime: ${typeof Bun !== 'undefined' ? 'Bun' : 'Node.js'} ${typeof Bun !== 'undefined' ? Bun.version : process.version}
203
+ CWD: ${process.cwd()}
204
+ `,
205
+ }
206
+ }
207
+
208
+ const costCmd: CommandHandler = (ctx) => {
209
+ const tokens = ctx.engine.getContext().getEstimatedTokens()
210
+ return {
211
+ content: stripIndent`
212
+ ── Token Usage (estimated) ──
213
+ Context tokens: ~${tokens.toLocaleString()} / 200,000
214
+ Usage: ${((tokens / 200_000) * 100).toFixed(1)}%
215
+
216
+ Token counting is approximate (chars/4).
217
+ Actual API usage depends on provider and model.
218
+ `,
219
+ }
220
+ }
221
+
222
+ // ═══════════════════════════════════════════════════════════════
223
+ // Model & Provider
224
+ // ═══════════════════════════════════════════════════════════════
225
+
226
+ const modelCmd: CommandHandler = (ctx) => ({
227
+ content: `Current model: ${ctx.modelId}\nProvider: ${ctx.providerId}\n\nUse /switch <provider> <model> to change.`,
228
+ })
229
+
230
+ const modelsCmd: CommandHandler = (ctx) => {
231
+ const lines = ctx.config.providers
232
+ .filter((p) => p.status !== 'upcoming')
233
+ .flatMap((p) =>
234
+ p.models
235
+ .filter((m) => m.status === 'active')
236
+ .map(
237
+ (m) =>
238
+ ` ${p.id.padEnd(12)} ${m.id.padEnd(30)} ${m.contextWindow.toLocaleString()} ctx ${m.vision ? '🖼' : '📝'}`,
239
+ ),
240
+ )
241
+
242
+ return {
243
+ content: `Available models (${lines.length} active):\n\nProvider Model Context Vision\n${'-'.repeat(80)}\n${lines.join('\n')}\n\nUse /switch <provider> <model> to change.\nSee /providers for upcoming models.`,
244
+ }
245
+ }
246
+
247
+ const providerCmd: CommandHandler = (ctx) => ({
248
+ content: `Current provider: ${ctx.providerId}\nModel: ${ctx.modelId}`,
249
+ })
250
+
251
+ const providersCmd: CommandHandler = (ctx) => {
252
+ const lines = ctx.config.providers.map(
253
+ (p) =>
254
+ ` ${p.id.padEnd(14)} ${p.name.padEnd(20)} ${p.protocol.padEnd(18)} ${p.models.length} models ${p.status === 'upcoming' ? '[upcoming]' : '✓'}`,
255
+ )
256
+ return {
257
+ content: `Configured providers:\n\n${lines.join('\n')}\n\nCurrent: ${ctx.providerId}/${ctx.modelId}`,
258
+ }
259
+ }
260
+
261
+ const switchCmd: CommandHandler = (ctx, args) => {
262
+ const [newProvider, newModel] = args
263
+ if (!newProvider || !newModel) {
264
+ return {
265
+ content: 'Usage: /switch <provider> <model>\nExample: /switch deepseek deepseek-v4-pro',
266
+ }
267
+ }
268
+ ctx.engine.switchProvider(newProvider, newModel)
269
+ return {
270
+ content: `✓ Switched to ${newProvider}/${newModel}`,
271
+ nextProvider: newProvider,
272
+ nextModel: newModel,
273
+ }
274
+ }
275
+
276
+ const configCmd: CommandHandler = (ctx) => {
277
+ const c = ctx.config
278
+ const lines = [
279
+ `version: ${c.version}`,
280
+ `defaultProvider: ${c.defaultProvider}`,
281
+ `defaultModel: ${c.defaultModel}`,
282
+ `permission: ${c.permission}`,
283
+ `providers: ${c.providers.length} configured`,
284
+ ]
285
+ return {
286
+ content: `── Configuration ──\n${lines.join('\n')}\n\nEdit: ~/.mipham/config.yml or .mipham/config.yml`,
287
+ }
288
+ }
289
+
290
+ // ═══════════════════════════════════════════════════════════════
291
+ // Tools & Skills
292
+ // ═══════════════════════════════════════════════════════════════
293
+
294
+ const toolsCmd: CommandHandler = (ctx) => {
295
+ const tools = ctx.engine.getTools()
296
+ const categories: Record<string, string[]> = {
297
+ file: [],
298
+ exec: [],
299
+ agent: [],
300
+ network: [],
301
+ system: [],
302
+ }
303
+ for (const [name, tool] of tools) {
304
+ categories[tool.category]?.push(
305
+ ` ${name.padEnd(14)} ${tool.permission.padEnd(8)} ${tool.description}`,
306
+ )
307
+ }
308
+ const sections = Object.entries(categories)
309
+ .filter(([, v]) => v!.length > 0)
310
+ .map(([cat, items]) => `── ${cat.toUpperCase()} ──\n${items!.join('\n')}`)
311
+
312
+ return { content: `Available tools (${tools.size}):\n\n${sections.join('\n\n')}` }
313
+ }
314
+
315
+ const skillsCmd: CommandHandler = (_ctx) => ({
316
+ content: stripIndent`
317
+ ── Standard Skills (12) ──
318
+ code-review Code review automation (v2.0)
319
+ compassionate-communication Warm, respectful, user-centered interaction
320
+ doc-generator Generate technical docs from code
321
+ github-ops GitHub PR/issues/releases management
322
+ memory Persistent memory read/write
323
+ mipham-code-setup Mipham Code install & config guide
324
+ security-review Security audit (OWASP, secrets, supply chain)
325
+ self-review Self-review diff for bugs and cleanup
326
+ superpower Skill discovery and usage guide
327
+ tdd Test-Driven Development workflow
328
+ web-search Web search for current information
329
+ code-review (classic) Legacy code review
330
+
331
+ ── Mipham Exclusive (2) ──
332
+ om-security Security analysis (injection, PII, adversarial)
333
+ om-model-optimize Model optimization (context, caching, tokens)
334
+
335
+ 14 skills loaded. Use Skill tool to invoke.
336
+ `,
337
+ })
338
+
339
+ // ═══════════════════════════════════════════════════════════════
340
+ // Workflow
341
+ // ═══════════════════════════════════════════════════════════════
342
+
343
+ const planCmd: CommandHandler = (_ctx) => ({
344
+ content: stripIndent`
345
+ ── Plan Mode ──
346
+ Plan mode activated — read-only analysis and design.
347
+ No code will be modified. Use /no-plan to exit.
348
+
349
+ In plan mode you can:
350
+ • Explore codebase and analyze architecture
351
+ • Design implementation approaches
352
+ • Create structured plans before coding
353
+ `,
354
+ })
355
+
356
+ const tddCmd: CommandHandler = (_ctx) => ({
357
+ content: stripIndent`
358
+ ── TDD Mode ── [stub]
359
+ Test-Driven Development workflow: RED → GREEN → REFACTOR.
360
+ Full TDD integration coming in a future release.
361
+ `,
362
+ })
363
+
364
+ const todosCmd: CommandHandler = (_ctx) => ({
365
+ content: stripIndent`
366
+ ── Task Management ──
367
+ Use the Task tool for structured task tracking:
368
+ • Create: Task tool with action=create
369
+ • List: Task tool with action=list
370
+ • Update: Task tool with action=update
371
+
372
+ Full /todos integration coming in a future release.
373
+ `,
374
+ })
375
+
376
+ // ═══════════════════════════════════════════════════════════════
377
+ // Project
378
+ // ═══════════════════════════════════════════════════════════════
379
+
380
+ const initCmd: CommandHandler = (_ctx) => ({
381
+ content: stripIndent`
382
+ ── Initialize Mipham Code ──
383
+
384
+ To initialize a project:
385
+
386
+ 1. Create .mipham/config.yml:
387
+ version: "0.1.0"
388
+ defaultProvider: anthropic
389
+ defaultModel: claude-sonnet-4-6
390
+ permission: auto
391
+
392
+ 2. Or use the Config tool:
393
+ Config set defaultProvider anthropic
394
+
395
+ Run /config to view current configuration.
396
+ `,
397
+ })
398
+
399
+ // ═══════════════════════════════════════════════════════════════
400
+ // Phase 1 — New Session Commands
401
+ // ═══════════════════════════════════════════════════════════════
402
+
403
+ const renameCmd: CommandHandler = (ctx, args) => {
404
+ const name = args.join(' ')
405
+ if (!name.trim()) {
406
+ return { content: 'Usage: /rename <session-name>\nExample: /rename Bug Fix Session' }
407
+ }
408
+ ctx.setSessionTitle(name.trim())
409
+ return { content: `✓ Session renamed to "${name.trim()}"` }
410
+ }
411
+
412
+ const goalCmd: CommandHandler = (ctx, args) => {
413
+ const goal = args.join(' ')
414
+ if (!goal.trim()) {
415
+ const c = ctx.engine.getContext()
416
+ return {
417
+ content: `Usage: /goal <statement>\n\nSet a session-level completion condition. Mipham Code will track progress toward this goal.\n\nExample: /goal Fix all TypeScript errors and make tests pass`,
418
+ }
419
+ }
420
+ ctx.setGoal(goal.trim())
421
+ return {
422
+ content: `✓ Goal set: "${goal.trim()}"\n\nUse /status to view progress. Type /goal without arguments to clear.`,
423
+ }
424
+ }
425
+
426
+ const recapCmd: CommandHandler = (ctx) => {
427
+ const c = ctx.engine.getContext()
428
+ const msgs = c.getMessages()
429
+ if (msgs.length === 0) {
430
+ return { content: 'No conversation to recap.' }
431
+ }
432
+ // Show summary of conversation: count messages, roles, estimated tokens
433
+ const userMsgs = msgs.filter((m) => m.role === 'user').length
434
+ const assistantMsgs = msgs.filter((m) => m.role === 'assistant').length
435
+ const tokens = c.getEstimatedTokens()
436
+ const checkpointCount = c.getCheckpoints().length
437
+
438
+ // Extract first few user messages as "topics"
439
+ const topics = msgs
440
+ .filter((m) => m.role === 'user' && typeof m.content === 'string')
441
+ .slice(0, 5)
442
+ .map((m) => {
443
+ const text = typeof m.content === 'string' ? m.content : ''
444
+ return text.length > 80 ? text.slice(0, 80) + '...' : text
445
+ })
446
+
447
+ return {
448
+ content: stripIndent`
449
+ ── Session Recap ──
450
+ Messages: ${msgs.length} (${userMsgs} user, ${assistantMsgs} assistant)
451
+ Est. tokens: ~${tokens.toLocaleString()}
452
+ Checkpoints: ${checkpointCount}
453
+
454
+ Recent topics:
455
+ ${topics.map((t, i) => ` ${i + 1}. ${t}`).join('\n')}
456
+
457
+ Use /rewind to undo, /clear to reset, /export to save.
458
+ `,
459
+ }
460
+ }
461
+
462
+ const usageCmd: CommandHandler = (ctx) => {
463
+ const c = ctx.engine.getContext()
464
+ const tokens = c.getEstimatedTokens()
465
+ const msgs = c.getMessages()
466
+ const maxTokens = 200_000
467
+ const pct = ((tokens / maxTokens) * 100).toFixed(1)
468
+
469
+ return {
470
+ content: stripIndent`
471
+ ── Usage Dashboard ──
472
+ Context tokens: ~${tokens.toLocaleString()} / ${maxTokens.toLocaleString()} (${pct}%)
473
+ Messages: ${msgs.length}
474
+ Provider: ${ctx.providerId}
475
+ Model: ${ctx.modelId}
476
+
477
+ ${'█'.repeat(Math.ceil(Number(pct) / 5))}${'░'.repeat(20 - Math.ceil(Number(pct) / 5))} ${pct}%
478
+
479
+ Note: Token counting is approximate (chars/4).
480
+ Use /context for detailed stats, /compact to free space.
481
+ `,
482
+ }
483
+ }
484
+
485
+ const reloadSkillsCmd: CommandHandler = (ctx) => {
486
+ if (!ctx.skillsLoader) {
487
+ return { content: 'SkillsLoader not available in this context.' }
488
+ }
489
+ try {
490
+ const config = ctx.config
491
+ // Re-load builtin skills from the skills directory
492
+ // The base path is typically relative to the CLI package
493
+ ctx.skillsLoader.loadBuiltin(process.cwd())
494
+ if (config.skills?.paths) {
495
+ ctx.skillsLoader.loadExternal(config.skills.paths)
496
+ }
497
+ const skills = ctx.skillsLoader.list()
498
+ return {
499
+ content: `✓ Skills reloaded — ${skills.length} loaded.\n\n${skills.map((s) => ` ${s.name.padEnd(28)} ${s.type.padEnd(10)} ${s.description}`).join('\n')}`,
500
+ }
501
+ } catch (err) {
502
+ return { content: `Failed to reload skills: ${String(err)}` }
503
+ }
504
+ }
505
+
506
+ // ═══════════════════════════════════════════════════════════════
507
+ // Phase 1 — History & Checkpoint Commands
508
+ // ═══════════════════════════════════════════════════════════════
509
+
510
+ const rewindCmd: CommandHandler = (ctx) => {
511
+ const c = ctx.engine.getContext()
512
+ const checkpoints = c.getCheckpoints()
513
+
514
+ if (checkpoints.length === 0) {
515
+ return {
516
+ content:
517
+ 'No checkpoints available. Checkpoints are automatically saved after each AI response.',
518
+ }
519
+ }
520
+
521
+ const result = c.restoreCheckpoint()
522
+ if (!result.restored) {
523
+ return { content: 'No checkpoint to restore.' }
524
+ }
525
+
526
+ return {
527
+ content: stripIndent`
528
+ ✓ Rewound to checkpoint "${result.label}"
529
+ Restored ${result.messageCount} messages.
530
+
531
+ Remaining checkpoints: ${c.getCheckpoints().length}
532
+ Use /rewind again to go back further, or continue chatting.
533
+ `,
534
+ clearMessages: true,
535
+ }
536
+ }
537
+
538
+ const undoCmd: CommandHandler = rewindCmd
539
+
540
+ const copyCmd: CommandHandler = (ctx, args) => {
541
+ const c = ctx.engine.getContext()
542
+ const msgs = c.getMessages()
543
+ const assistantMsgs = msgs.filter((m) => m.role === 'assistant')
544
+
545
+ if (assistantMsgs.length === 0) {
546
+ return { content: 'No assistant responses to copy.' }
547
+ }
548
+
549
+ // Determine which response to copy: last N or last 1
550
+ let n = 1
551
+ if (args[0]) {
552
+ n = parseInt(args[0]!, 10)
553
+ if (isNaN(n) || n < 1) {
554
+ return {
555
+ content: 'Usage: /copy [N]\nN = number of recent assistant responses to copy (default: 1)',
556
+ }
557
+ }
558
+ }
559
+
560
+ const toCopy = assistantMsgs.slice(-n)
561
+ const text = toCopy
562
+ .map((m) => (typeof m.content === 'string' ? m.content : JSON.stringify(m.content)))
563
+ .join('\n\n---\n\n')
564
+
565
+ return {
566
+ content: `✓ Copied ${toCopy.length} assistant response(s) to clipboard (${text.length.toLocaleString()} chars).`,
567
+ copyContent: text,
568
+ }
569
+ }
570
+
571
+ const diffCmd: CommandHandler = async (_ctx) => {
572
+ try {
573
+ const { execSync } = await import('node:child_process')
574
+ const output = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 })
575
+ if (!output.trim()) {
576
+ return { content: 'No uncommitted changes (working tree clean).' }
577
+ }
578
+ // Get full diff but limit to reasonable size
579
+ const fullDiff = execSync('git diff --no-color', { encoding: 'utf-8', timeout: 5000 })
580
+ const MAX_LINES = 60
581
+ const lines = fullDiff.split('\n')
582
+ const truncated =
583
+ lines.length > MAX_LINES
584
+ ? lines.slice(0, MAX_LINES).join('\n') +
585
+ `\n\n... (${lines.length - MAX_LINES} more lines. Use git diff to see full output.)`
586
+ : fullDiff
587
+
588
+ return {
589
+ content: `── Git Diff ──\n\n${truncated}`,
590
+ }
591
+ } catch {
592
+ return {
593
+ content: 'Unable to run git diff. Ensure git is installed and you are in a repository.',
594
+ }
595
+ }
596
+ }
597
+
598
+ // ═══════════════════════════════════════════════════════════════
599
+ // Phase 1 — Model Control Commands
600
+ // ═══════════════════════════════════════════════════════════════
601
+
602
+ const fastCmd: CommandHandler = (ctx, args) => {
603
+ const arg = args[0]?.toLowerCase()
604
+ if (arg === 'on') {
605
+ ctx.setFastMode(true)
606
+ return { content: '✓ Fast mode ON — responses will prioritize speed over depth.' }
607
+ } else if (arg === 'off') {
608
+ ctx.setFastMode(false)
609
+ return { content: '✓ Fast mode OFF — standard quality mode.' }
610
+ } else if (arg) {
611
+ return { content: 'Usage: /fast [on|off]\nToggle fast mode for faster responses.' }
612
+ } else {
613
+ // Toggle
614
+ // We can't read current state from context, so we just show usage
615
+ return {
616
+ content:
617
+ 'Usage: /fast [on|off]\n\nFast mode prioritizes speed over depth. Currently available as a configuration toggle.\n\nExample:\n /fast on — enable fast mode\n /fast off — disable fast mode',
618
+ }
619
+ }
620
+ }
621
+
622
+ const effortCmd: CommandHandler = (ctx, args) => {
623
+ const VALID_LEVELS = ['low', 'medium', 'high', 'xhigh', 'max']
624
+ const level = args[0]?.toLowerCase()
625
+
626
+ if (!level || !VALID_LEVELS.includes(level)) {
627
+ return {
628
+ content: stripIndent`
629
+ Usage: /effort <level>
630
+
631
+ Set reasoning effort level:
632
+ low — Fast, simple tasks
633
+ medium — Balanced quality and speed
634
+ high — Thorough reasoning (default)
635
+ xhigh — Maximum depth for coding/agentic use
636
+ max — Absolute ceiling, very thorough
637
+
638
+ Current model: ${ctx.modelId}
639
+ Effort levels require compatible providers (Anthropic Opus 4.6+, Sonnet 4.6).
640
+ `,
641
+ }
642
+ }
643
+
644
+ ctx.setEffort(level)
645
+ return { content: `✓ Reasoning effort set to "${level}"` }
646
+ }
647
+
648
+ const focusCmd: CommandHandler = (ctx) => {
649
+ ctx.setFocusMode(true)
650
+ return {
651
+ content: stripIndent`
652
+ ✓ Focus mode ON — showing only the most recent exchange.
653
+ Previous messages are hidden but preserved.
654
+ Type /focus again to exit focus mode and show all messages.
655
+ `,
656
+ }
657
+ }
658
+
659
+ // ═══════════════════════════════════════════════════════════════
660
+ // Phase 1 — Workflow Commands
661
+ // ═══════════════════════════════════════════════════════════════
662
+
663
+ const tasksCmd: CommandHandler = (ctx) => {
664
+ const c = ctx.engine.getContext()
665
+ const msgs = c.getMessages()
666
+
667
+ // Scan for task-related tool uses in message history
668
+ const toolUses = msgs.flatMap((m) => {
669
+ if (Array.isArray(m.content)) {
670
+ return m.content.filter(
671
+ (b) => b.type === 'tool_use' && ['TaskCreate', 'TaskUpdate', 'TaskList'].includes(b.name),
672
+ )
673
+ }
674
+ return []
675
+ })
676
+
677
+ return {
678
+ content: stripIndent`
679
+ ── Background Tasks ──
680
+
681
+ ${
682
+ toolUses.length > 0
683
+ ? `${toolUses.length} task operations detected in this session.\n\nUse Task tool (TaskCreate / TaskUpdate / TaskList) to manage structured task tracking.`
684
+ : 'No tasks tracked yet. Use TaskCreate, TaskUpdate, and TaskList tools to manage structured tasks.'
685
+ }
686
+
687
+ Quick reference:
688
+ TaskCreate — create a new task
689
+ TaskList — list all tasks
690
+ TaskUpdate — update task status
691
+ TaskGet — get task details
692
+ TaskOutput — get background task output
693
+ TaskStop — stop a running task
694
+
695
+ Type /todos for the legacy task interface.
696
+ `,
697
+ }
698
+ }
699
+
700
+ const branchCmd: CommandHandler = (ctx, args) => {
701
+ const name = args.join(' ') || `branch-${Date.now()}`
702
+ const c = ctx.engine.getContext()
703
+ const msgs = c.getMessages()
704
+
705
+ if (msgs.length === 0) {
706
+ return { content: 'No conversation to branch. Start a conversation first.' }
707
+ }
708
+
709
+ // Save current session state as a named checkpoint
710
+ const checkpointId = c.saveCheckpoint(name)
711
+ return {
712
+ content: stripIndent`
713
+ ── Branch Created ──
714
+ Name: "${name}"
715
+ Checkpoint: #${checkpointId}
716
+ Messages: ${msgs.length} saved
717
+
718
+ Current conversation continues from this point.
719
+ To return to this branch point later, use:
720
+ /rewind
721
+
722
+ Note: Full session branching (separate concurrent sessions) requires session persistence, coming in a future release. For now, this saves a named checkpoint you can rewind to.
723
+ `,
724
+ }
725
+ }
726
+
727
+ const loopCmd: CommandHandler = (_ctx, args) => {
728
+ if (args.length < 2) {
729
+ return {
730
+ content: stripIndent`
731
+ Usage: /loop <interval> <prompt>
732
+
733
+ Run a prompt repeatedly on a set interval.
734
+
735
+ Interval formats:
736
+ 10s — 10 seconds
737
+ 5m — 5 minutes
738
+ 1h — 1 hour
739
+
740
+ Example:
741
+ /loop 5m check the deploy status
742
+ /loop 1h run the smoke tests
743
+
744
+ Note: Continuous looping requires session persistence, coming in a future release.
745
+ For now, this sets up the intent — you'll be prompted to confirm each iteration.
746
+ `,
747
+ }
748
+ }
749
+
750
+ const interval = args[0]!
751
+ const prompt = args.slice(1).join(' ')
752
+
753
+ return {
754
+ content: stripIndent`
755
+ ── Loop Configured ──
756
+ Interval: ${interval}
757
+ Prompt: "${prompt}"
758
+
759
+ Loop scheduling infrastructure is being built.
760
+ For now, manually re-run: /loop ${interval} ${prompt}
761
+ `,
762
+ }
763
+ }
764
+
765
+ const scheduleCmd: CommandHandler = (_ctx) => ({
766
+ content: stripIndent`
767
+ ── Scheduled Tasks ──
768
+
769
+ No scheduled tasks configured.
770
+
771
+ Schedule infrastructure is being built. Coming features:
772
+ • Cron-based scheduling (CronCreate tool)
773
+ • One-shot and recurring tasks
774
+ • Durable persistence across sessions
775
+
776
+ Use /loop for simple repetition in the current session.
777
+ `,
778
+ })
779
+
780
+ // ═══════════════════════════════════════════════════════════════
781
+ // Diagnostic
782
+ // ═══════════════════════════════════════════════════════════════
783
+
784
+ const doctorCmd: CommandHandler = async (ctx) => {
785
+ const lines: string[] = [
786
+ '── System Diagnostics ──',
787
+ '',
788
+ `Mipham Code v${ctx.version}`,
789
+ `Runtime ${typeof Bun !== 'undefined' ? 'Bun ' + Bun.version : 'Node.js ' + process.version}`,
790
+ `Platform ${process.platform} ${process.arch}`,
791
+ `CWD ${process.cwd()}`,
792
+ `PID ${process.pid}`,
793
+ '',
794
+ '── Config ──',
795
+ `Provider ${ctx.providerId} / ${ctx.modelId}`,
796
+ `Permission ${ctx.config.permission}`,
797
+ `Providers ${ctx.config.providers.length} configured (${ctx.config.providers.filter((p) => p.status !== 'upcoming').length} active)`,
798
+ '',
799
+ '── Session ──',
800
+ ]
801
+
802
+ const c = ctx.engine.getContext()
803
+ const msgs = c.getMessages()
804
+ const tokens = c.getEstimatedTokens()
805
+ lines.push(`Messages ${msgs.length}`)
806
+ lines.push(
807
+ `Tokens ~${tokens.toLocaleString()} / 200,000 (${((tokens / 200_000) * 100).toFixed(1)}%)`,
808
+ )
809
+ lines.push(`Checkpoints ${c.getCheckpoints().length}`)
810
+
811
+ // Git info
812
+ try {
813
+ const { execSync } = await import('node:child_process')
814
+ lines.push('')
815
+ lines.push('── Git ──')
816
+ const branch = execSync('git branch --show-current', {
817
+ encoding: 'utf-8',
818
+ timeout: 3000,
819
+ }).trim()
820
+ lines.push(`Branch ${branch || '(detached)'}`)
821
+ const status = execSync('git status --porcelain', { encoding: 'utf-8', timeout: 3000 })
822
+ const changed = status.trim().split('\n').filter(Boolean).length
823
+ lines.push(`Changed ${changed} file${changed !== 1 ? 's' : ''}`)
824
+ const log = execSync('git log --oneline -3', { encoding: 'utf-8', timeout: 3000 }).trim()
825
+ lines.push(`Last commits ${log.split('\n').length}`)
826
+ lines.push('')
827
+ lines.push(
828
+ log
829
+ .split('\n')
830
+ .map((l, i) => ` ${i + 1}. ${l}`)
831
+ .join('\n'),
832
+ )
833
+ } catch {
834
+ lines.push('')
835
+ lines.push('── Git ──')
836
+ lines.push(' (not a git repository or git not available)')
837
+ }
838
+
839
+ // Skills info
840
+ if (ctx.skillsLoader) {
841
+ lines.push('')
842
+ lines.push('── Skills ──')
843
+ try {
844
+ const skills = ctx.skillsLoader.list()
845
+ const standard = skills.filter((s: { type: string }) => s.type === 'standard').length
846
+ const mipham = skills.filter((s: { type: string }) => s.type === 'mipham').length
847
+ lines.push(`Loaded ${skills.length} (${standard} standard + ${mipham} mipham)`)
848
+ } catch {
849
+ lines.push(' (skills info unavailable)')
850
+ }
851
+ }
852
+
853
+ return { content: lines.join('\n') }
854
+ }
855
+
856
+ // ═══════════════════════════════════════════════════════════════
857
+ // Export
858
+ // ═══════════════════════════════════════════════════════════════
859
+
860
+ const exportCmd: CommandHandler = async (ctx) => {
861
+ const { writeFileSync, existsSync } = await import('node:fs')
862
+ const { join } = await import('node:path')
863
+
864
+ const cwd = process.cwd()
865
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)
866
+ const filename = `mipham-export-${timestamp}.md`
867
+ const filepath = join(cwd, filename)
868
+
869
+ const msgs = ctx.engine.getContext().getMessages()
870
+ const lines: string[] = [
871
+ `# Mipham Code — Session Export`,
872
+ `> ${new Date().toISOString()}`,
873
+ `> Provider: ${ctx.providerId} / ${ctx.modelId}`,
874
+ '',
875
+ '---',
876
+ '',
877
+ ]
878
+
879
+ for (const msg of msgs) {
880
+ const roleLabel =
881
+ msg.role === 'user' ? '🧑 User' : msg.role === 'assistant' ? '🤖 Mipham Code' : '⚠ System'
882
+ const content = typeof msg.content === 'string' ? msg.content : JSON.stringify(msg.content)
883
+ lines.push(`### ${roleLabel}`)
884
+ lines.push('')
885
+ lines.push(content)
886
+ lines.push('')
887
+ }
888
+
889
+ writeFileSync(filepath, lines.join('\n'), 'utf-8')
890
+ return {
891
+ content: `✓ Session exported to:\n ${filepath}\n\n${msgs.length} messages · ${lines.length} lines`,
892
+ copyContent: filepath,
893
+ }
894
+ }
895
+
896
+ // ═══════════════════════════════════════════════════════════════
897
+ // Review — code review workflow
898
+ // ═══════════════════════════════════════════════════════════════
899
+
900
+ const reviewCmd: CommandHandler = async () => {
901
+ try {
902
+ const { execSync } = await import('node:child_process')
903
+ const diff = execSync('git diff --stat', { encoding: 'utf-8', timeout: 5000 }).trim()
904
+ const unstaged = execSync('git diff --name-only', { encoding: 'utf-8', timeout: 3000 }).trim()
905
+ const staged = execSync('git diff --cached --name-only', {
906
+ encoding: 'utf-8',
907
+ timeout: 3000,
908
+ }).trim()
909
+
910
+ if (!diff) {
911
+ return {
912
+ content:
913
+ '─ Code Review ─\n\nNo uncommitted changes detected.\n\nUse /pr-comments for PR-level review, or make changes first.',
914
+ }
915
+ }
916
+
917
+ const lines: string[] = ['─ Code Review ─', '', 'Uncommitted changes:', '', diff]
918
+
919
+ if (staged) {
920
+ lines.push('')
921
+ lines.push('Staged files (ready for commit):')
922
+ for (const f of staged.split('\n')) lines.push(` ✓ ${f}`)
923
+ }
924
+ if (unstaged) {
925
+ lines.push('')
926
+ lines.push('Unstaged files (working directory):')
927
+ for (const f of unstaged.split('\n')) lines.push(` • ${f}`)
928
+ }
929
+
930
+ lines.push('')
931
+ lines.push('To review with AI: type "review these changes" in chat.')
932
+ lines.push('To commit: git add -A && git commit -m "..."')
933
+
934
+ return { content: lines.join('\n') }
935
+ } catch {
936
+ return { content: '─ Code Review ─\n\nCould not run git diff. Are you in a git repository?' }
937
+ }
938
+ }
939
+
940
+ // ═══════════════════════════════════════════════════════════════
941
+ // PR Comments
942
+ // ═══════════════════════════════════════════════════════════════
943
+
944
+ const prCommentsCmd: CommandHandler = async () => {
945
+ try {
946
+ const { execSync } = await import('node:child_process')
947
+
948
+ // Get branch info
949
+ const branch = execSync('git branch --show-current', {
950
+ encoding: 'utf-8',
951
+ timeout: 3000,
952
+ }).trim()
953
+ const mainBranch =
954
+ execSync('git remote show origin 2>/dev/null | grep "HEAD branch" | cut -d: -f2', {
955
+ encoding: 'utf-8',
956
+ timeout: 3000,
957
+ }).trim() || 'main'
958
+
959
+ // Get diff stats vs main
960
+ const diffStat = execSync(
961
+ `git diff --stat origin/${mainBranch}...HEAD 2>/dev/null || git diff --stat ${mainBranch}...HEAD 2>/dev/null || echo "(no remote tracking)"`,
962
+ { encoding: 'utf-8', timeout: 5000 },
963
+ ).trim()
964
+ const commits = execSync(
965
+ `git log --oneline origin/${mainBranch}..HEAD 2>/dev/null || git log --oneline ${mainBranch}..HEAD 2>/dev/null || echo "(no commits ahead)"`,
966
+ { encoding: 'utf-8', timeout: 5000 },
967
+ ).trim()
968
+
969
+ const lines: string[] = [
970
+ '─ PR Review ─',
971
+ '',
972
+ `Branch: ${branch}`,
973
+ `Base: ${mainBranch}`,
974
+ `Commits ahead: ${commits.split('\n').filter(Boolean).length}`,
975
+ '',
976
+ ]
977
+
978
+ if (commits && commits !== '(no commits ahead)') {
979
+ lines.push('Commits:')
980
+ for (const c of commits.split('\n')) lines.push(` ${c}`)
981
+ lines.push('')
982
+ }
983
+
984
+ if (diffStat && diffStat !== '(no remote tracking)') {
985
+ lines.push('Changed files:')
986
+ lines.push(diffStat)
987
+ lines.push('')
988
+ }
989
+
990
+ lines.push(
991
+ 'To generate PR description: type "write a PR description for these changes" in chat.',
992
+ )
993
+ lines.push('To review PR: type "review this PR" or use /review.')
994
+
995
+ return { content: lines.join('\n') }
996
+ } catch {
997
+ return {
998
+ content:
999
+ '─ PR Review ─\n\nCould not determine PR context. Are you in a git repository with a remote?',
1000
+ }
1001
+ }
1002
+ }
1003
+
1004
+ // ═══════════════════════════════════════════════════════════════
1005
+ // Resume
1006
+ // ═══════════════════════════════════════════════════════════════
1007
+
1008
+ const resumeCmd: CommandHandler = async (_ctx, args) => {
1009
+ const { SessionStore } = await import('../core/session-store')
1010
+
1011
+ // If a name is provided, show load instructions
1012
+ const targetName = args.join(' ')
1013
+ if (targetName) {
1014
+ const session = SessionStore.load(targetName)
1015
+ if (session) {
1016
+ return {
1017
+ content: [
1018
+ '─ Session Found ─',
1019
+ '',
1020
+ `Name: ${session.metadata.name}`,
1021
+ `Messages: ${session.metadata.messageCount}`,
1022
+ `Provider: ${session.metadata.provider} / ${session.metadata.model}`,
1023
+ `Updated: ${session.metadata.updatedAt}`,
1024
+ '',
1025
+ 'To resume this session:',
1026
+ ` mipham --resume "${targetName}"`,
1027
+ '',
1028
+ 'Or restart Mipham Code — the most recent session loads automatically.',
1029
+ ].join('\n'),
1030
+ }
1031
+ }
1032
+ return {
1033
+ content: `─ Session Not Found ─\n\nNo session named "${targetName}".\n\nUse /resume without arguments to list all saved sessions.`,
1034
+ }
1035
+ }
1036
+
1037
+ const sessions = SessionStore.list()
1038
+
1039
+ if (sessions.length === 0) {
1040
+ return {
1041
+ content:
1042
+ '─ Resume Session ─\n\nNo saved sessions found.\n\nSessions are auto-saved to ~/.mipham/sessions/ when Mipham Code exits.\nStart a conversation — it will be saved automatically.',
1043
+ }
1044
+ }
1045
+
1046
+ const recent = sessions.slice(0, 10)
1047
+
1048
+ const lines: string[] = [
1049
+ '─ Saved Sessions ─',
1050
+ '',
1051
+ ...recent.map(
1052
+ (s, i) =>
1053
+ ` ${(i + 1).toString().padStart(2)}. ${s.name.padEnd(45)} ${s.messageCount.toString().padStart(4)} msgs ${new Date(s.updatedAt).toLocaleString()}`,
1054
+ ),
1055
+ '',
1056
+ `Total: ${sessions.length} session(s) • Location: ~/.mipham/sessions/`,
1057
+ '',
1058
+ 'To resume a session: /resume <name>',
1059
+ 'To resume from CLI: mipham --resume "<name>"',
1060
+ '',
1061
+ 'Sessions are auto-saved on exit. The most recent session loads automatically on restart.',
1062
+ ]
1063
+
1064
+ return { content: lines.join('\n') }
1065
+ }
1066
+
1067
+ // ═══════════════════════════════════════════════════════════════
1068
+ // Memory Management
1069
+ // ═══════════════════════════════════════════════════════════════
1070
+
1071
+ const memoryCmd: CommandHandler = async () => {
1072
+ const { existsSync, readdirSync, readFileSync, statSync } = await import('node:fs')
1073
+ const { join } = await import('node:path')
1074
+
1075
+ const home = process.env.HOME || '~'
1076
+ const memoryDir = join(home, '.mipham', 'memory')
1077
+
1078
+ if (!existsSync(memoryDir)) {
1079
+ return {
1080
+ content:
1081
+ '─ Memory ─\n\nNo memories stored yet.\n\nMemory is saved to ~/.mipham/memory/ by the AI when you ask it to remember something.\nTry: "remember that I prefer TypeScript"',
1082
+ }
1083
+ }
1084
+
1085
+ try {
1086
+ const files = readdirSync(memoryDir).filter((f) => f.endsWith('.md'))
1087
+ if (files.length === 0) {
1088
+ return { content: '─ Memory ─\n\nNo memory files found in ~/.mipham/memory/' }
1089
+ }
1090
+
1091
+ const memories: Array<{ file: string; size: number; mtime: Date; title: string }> = []
1092
+ for (const f of files) {
1093
+ const p = join(memoryDir, f)
1094
+ const stat = statSync(p)
1095
+ let title = f
1096
+ try {
1097
+ const content = readFileSync(p, 'utf-8')
1098
+ const match = content.match(/^#\s+(.+)$/m)
1099
+ if (match) title = match[1]!
1100
+ } catch {
1101
+ /* use filename */
1102
+ }
1103
+ memories.push({ file: f, size: stat.size, mtime: stat.mtime, title })
1104
+ }
1105
+
1106
+ memories.sort((a, b) => b.mtime.getTime() - a.mtime.getTime())
1107
+
1108
+ const lines: string[] = [
1109
+ '─ Memory ─',
1110
+ '',
1111
+ `Location: ${memoryDir}`,
1112
+ `Total: ${memories.length} memor${memories.length === 1 ? 'y' : 'ies'}`,
1113
+ '',
1114
+ ...memories.map(
1115
+ (m, i) =>
1116
+ ` ${(i + 1).toString().padStart(2)}. ${m.file.padEnd(35)} ${(m.size / 1024).toFixed(1)}KB ${m.mtime.toLocaleDateString()} ${m.title}`,
1117
+ ),
1118
+ '',
1119
+ 'Memories are used by the AI to provide personalized context across sessions.',
1120
+ 'Each file in ~/.mipham/memory/ represents one remembered fact or preference.',
1121
+ ]
1122
+
1123
+ return { content: lines.join('\n') }
1124
+ } catch {
1125
+ return { content: '─ Memory ─\n\nCould not read memory directory.' }
1126
+ }
1127
+ }
1128
+
1129
+ // ═══════════════════════════════════════════════════════════════
1130
+ // Upgrade
1131
+ // ═══════════════════════════════════════════════════════════════
1132
+
1133
+ const upgradeCmd: CommandHandler = () => ({
1134
+ content: `─ Upgrade Mipham Code ─
1135
+
1136
+ Current version: v0.1.0
1137
+
1138
+ To upgrade:
1139
+ curl -fsSL https://mipham.ai/install.sh | bash
1140
+
1141
+ Or if installed via npm:
1142
+ npm update -g @mipham/cli
1143
+
1144
+ Release channels:
1145
+ • stable — recommended for most users
1146
+ • beta — early access to new features
1147
+ • nightly — latest commits, may be unstable
1148
+
1149
+ Check for updates: https://mipham.ai/code/releases`,
1150
+ })
1151
+
1152
+ // ═══════════════════════════════════════════════════════════════
1153
+ // No-Plan — exit plan mode
1154
+ // ═══════════════════════════════════════════════════════════════
1155
+
1156
+ const noPlanCmd: CommandHandler = () => ({
1157
+ content:
1158
+ '✓ Plan mode exited. Your plan has been discarded.\n\nContinue chatting as normal, or type /plan to start a new plan.',
1159
+ })
1160
+
1161
+ // ═══════════════════════════════════════════════════════════════
1162
+ // Workflows
1163
+ // ═══════════════════════════════════════════════════════════════
1164
+
1165
+ const workflowsCmd: CommandHandler = async () => {
1166
+ const { existsSync, readdirSync, readFileSync } = await import('node:fs')
1167
+ const { join } = await import('node:path')
1168
+
1169
+ const locations = [
1170
+ join(process.cwd(), '.claude', 'workflows'),
1171
+ join(process.env.HOME || '~', '.claude', 'workflows'),
1172
+ ]
1173
+
1174
+ const lines: string[] = ['─ Workflows ─', '']
1175
+ let found = 0
1176
+
1177
+ for (const loc of locations) {
1178
+ if (!existsSync(loc)) continue
1179
+ try {
1180
+ const items = readdirSync(loc).filter((f) => f.endsWith('.js') || f.endsWith('.ts'))
1181
+ if (items.length === 0) continue
1182
+
1183
+ lines.push(`📍 ${loc}`)
1184
+ for (const item of items) {
1185
+ const path = join(loc, item)
1186
+ try {
1187
+ const content = readFileSync(path, 'utf-8')
1188
+ const metaMatch = content.match(
1189
+ /export const meta\s*=\s*\{[^}]*name:\s*['"]([^'"]+)['"][^}]*description:\s*['"]([^'"]+)['"]/,
1190
+ )
1191
+ if (metaMatch) {
1192
+ lines.push(` • ${item} — "${metaMatch[1]}" — ${metaMatch[2]}`)
1193
+ } else {
1194
+ lines.push(` • ${item}`)
1195
+ }
1196
+ } catch {
1197
+ lines.push(` • ${item}`)
1198
+ }
1199
+ found++
1200
+ }
1201
+ } catch {
1202
+ /* skip */
1203
+ }
1204
+ }
1205
+
1206
+ if (found === 0) {
1207
+ lines.push('No workflow scripts found.')
1208
+ lines.push('')
1209
+ lines.push('Workflows are multi-agent orchestration scripts stored in:')
1210
+ lines.push(' .claude/workflows/ (project-level)')
1211
+ lines.push(' ~/.claude/workflows/ (user-level)')
1212
+ lines.push('')
1213
+ lines.push('Create a .js file in either location to add a workflow.')
1214
+ } else {
1215
+ lines.push('')
1216
+ lines.push(`${found} workflow(s) found.`)
1217
+ lines.push('Use /workflows <name> to run a specific workflow.')
1218
+ }
1219
+
1220
+ return { content: lines.join('\n') }
1221
+ }
1222
+
1223
+ // ═══════════════════════════════════════════════════════════════
1224
+ // Permissions
1225
+ // ═══════════════════════════════════════════════════════════════
1226
+
1227
+ const permissionsCmd: CommandHandler = (ctx) => {
1228
+ const c = ctx.engine.getContext()
1229
+ const msgs = c.getMessages()
1230
+
1231
+ return {
1232
+ content: `─ Permission Settings ─
1233
+
1234
+ Mode: ${ctx.config.permission}
1235
+ Messages: ${msgs.length} in context
1236
+ Tools: ${ctx.engine.getTools().size} available
1237
+
1238
+ Permission levels:
1239
+ auto — run tools automatically without asking
1240
+ ask — prompt before each tool execution (default)
1241
+ bypass — skip all permission checks (use with caution)
1242
+
1243
+ Change with /config permission <level>.
1244
+
1245
+ Current directory permissions:
1246
+ CWD: ${process.cwd()}
1247
+
1248
+ To add a directory: use /add-dir (coming soon).
1249
+ Tool execution is sandboxed to the project directory by default.`,
1250
+ }
1251
+ }
1252
+
1253
+ // ═══════════════════════════════════════════════════════════════
1254
+ // Setup — guided project initialization (mirrors Claude Code /setup)
1255
+ // ═══════════════════════════════════════════════════════════════
1256
+
1257
+ const setupCmd: CommandHandler = async (ctx, args) => {
1258
+ const step = args[0]
1259
+
1260
+ // ── Step selection ──
1261
+ if (step === '1' || step === 'init') {
1262
+ return setupStep1(ctx)
1263
+ }
1264
+ if (step === '2' || step === 'providers') {
1265
+ return setupStep2(ctx)
1266
+ }
1267
+ if (step === '3' || step === 'model') {
1268
+ return setupStep3(ctx)
1269
+ }
1270
+ if (step === '4' || step === 'skills') {
1271
+ return setupStep4(ctx)
1272
+ }
1273
+ if (step === '5' || step === 'permissions') {
1274
+ return setupStep5(ctx)
1275
+ }
1276
+ if (step === '6' || step === 'shell') {
1277
+ return setupStep6(ctx)
1278
+ }
1279
+
1280
+ // ── Check existing setup status ──
1281
+ const { existsSync } = await import('node:fs')
1282
+ const { join } = await import('node:path')
1283
+ const cwd = process.cwd()
1284
+ const home = process.env.HOME || '~'
1285
+
1286
+ const hasProjectMipham = existsSync(join(cwd, 'MIPHAM.md'))
1287
+ const hasProjectConfig = existsSync(join(cwd, '.mipham', 'config.yml'))
1288
+ const hasUserConfig = existsSync(join(home, '.mipham', 'config.yml'))
1289
+ const hasMiphamDir = existsSync(join(cwd, '.mipham'))
1290
+
1291
+ const activeProviders = ctx.config.providers.filter((p) => p.status === 'active').length
1292
+ const totalProviders = ctx.config.providers.length
1293
+ const skills = ctx.skillsLoader?.list() || []
1294
+ const standardSkills = skills.filter((s: { type: string }) => s.type === 'standard').length
1295
+ const miphamSkills = skills.filter((s: { type: string }) => s.type === 'mipham').length
1296
+
1297
+ const statusIcon = (ok: boolean) => (ok ? '✅' : '⬜')
1298
+
1299
+ return {
1300
+ content: `── Mipham Code Setup ──
1301
+
1302
+
1303
+ Project Status
1304
+ ${statusIcon(hasMiphamDir)} .mipham/ directory ${hasMiphamDir ? '(config + metadata)' : '(not created)'}
1305
+ ${statusIcon(hasProjectMipham)} MIPHAM.md ${hasProjectMipham ? '(project personality)' : '(not created)'}
1306
+ ${statusIcon(hasProjectConfig)} Project config ${hasProjectConfig ? '~/.mipham/config.yml' : '(not created)'}
1307
+ ${statusIcon(hasUserConfig)} User config ${hasUserConfig ? '~/.mipham/config.yml' : '(not created)'}
1308
+
1309
+ Providers
1310
+ ${activeProviders}/${totalProviders} active · Current: ${ctx.providerId}/${ctx.modelId}
1311
+
1312
+ Skills
1313
+ ${skills.length} loaded (${standardSkills} standard + ${miphamSkills} mipham)
1314
+
1315
+ Permissions
1316
+ Mode: ${ctx.config.permission} · Tools: ${ctx.engine.getTools().size}
1317
+
1318
+
1319
+ ── Setup Steps ──
1320
+
1321
+ 1. Initialize Project /setup 1 Create .mipham/ + MIPHAM.md + config.yml
1322
+ 2. Configure Providers /setup 2 Set API keys, enable/disable providers
1323
+ 3. Set Default Model /setup 3 Choose your preferred provider & model
1324
+ 4. Install Skills /setup 4 Browse and install community skills
1325
+ 5. Permissions Setup /setup 5 Configure tool access and security
1326
+ 6. Shell Integration /setup 6 Add \`mipham\` to PATH, IDE setup
1327
+
1328
+ Run: /setup <number> or chat: "help me set up Mipham Code"`,
1329
+ }
1330
+ }
1331
+
1332
+ async function setupStep1(ctx: CommandContext): Promise<CommandResult> {
1333
+ const { existsSync, mkdirSync, writeFileSync } = await import('node:fs')
1334
+ const { join } = await import('node:path')
1335
+ const cwd = process.cwd()
1336
+
1337
+ const miphamDir = join(cwd, '.mipham')
1338
+ const miphamPath = join(cwd, 'MIPHAM.md')
1339
+ const configPath = join(miphamDir, 'config.yml')
1340
+
1341
+ const created: string[] = []
1342
+ const skipped: string[] = []
1343
+
1344
+ // Create .mipham/ directory
1345
+ if (!existsSync(miphamDir)) {
1346
+ mkdirSync(miphamDir, { recursive: true })
1347
+ created.push('.mipham/')
1348
+ } else {
1349
+ skipped.push('.mipham/ (already exists)')
1350
+ }
1351
+
1352
+ // Create project config if missing
1353
+ if (!existsSync(configPath)) {
1354
+ const defaultConfig = `# Mipham Code — Project Configuration
1355
+ # See https://mipham.ai/code/docs/config for all options
1356
+
1357
+ defaultProvider: ${ctx.providerId}
1358
+ defaultModel: ${ctx.modelId}
1359
+ permission: ask
1360
+
1361
+ # Uncomment to add project-specific providers:
1362
+ # providers:
1363
+ # - id: anthropic
1364
+ # apiKey: \$ANTHROPIC_API_KEY
1365
+ `
1366
+ writeFileSync(configPath, defaultConfig, 'utf-8')
1367
+ created.push('.mipham/config.yml')
1368
+ } else {
1369
+ skipped.push('.mipham/config.yml (already exists)')
1370
+ }
1371
+
1372
+ // Create MIPHAM.md if missing
1373
+ if (!existsSync(miphamPath)) {
1374
+ const projectName = cwd.split('/').pop() || 'my-project'
1375
+ const defaultMipham = `---
1376
+ model: mipham-code
1377
+ version: 1.0.0
1378
+ privacy: project
1379
+ language: zh-CN
1380
+ ---
1381
+
1382
+ # MIPHAM.md — ${projectName}
1383
+
1384
+ > 本文件定义 ${projectName} 项目中 AI 助手的交互人格和项目规范。
1385
+ > 继承自 One Mipham Corporation 集团 MIPHAM.md。
1386
+
1387
+ ---
1388
+
1389
+ ## 项目概述
1390
+
1391
+ [简要描述项目目的和定位]
1392
+
1393
+ ## 技术栈
1394
+
1395
+ [列出主要技术栈]
1396
+
1397
+ ## 项目规范
1398
+
1399
+ - [添加项目特有的编码规则]
1400
+ - [添加团队约定]
1401
+
1402
+ ## AI 交互偏好
1403
+
1404
+ - 回复语言:[中文/英文]
1405
+ - 代码风格:[偏好]
1406
+ - 注释语言:[中文/英文]
1407
+ `
1408
+ writeFileSync(miphamPath, defaultMipham, 'utf-8')
1409
+ created.push('MIPHAM.md')
1410
+ } else {
1411
+ skipped.push('MIPHAM.md (already exists)')
1412
+ }
1413
+
1414
+ const lines: string[] = ['── Step 1: Initialize Project ──', '']
1415
+ if (created.length > 0) {
1416
+ lines.push('Created:')
1417
+ for (const c of created) lines.push(` ✅ ${c}`)
1418
+ }
1419
+ if (skipped.length > 0) {
1420
+ lines.push('')
1421
+ lines.push('Skipped (already configured):')
1422
+ for (const s of skipped) lines.push(` ⏭ ${s}`)
1423
+ }
1424
+ lines.push('')
1425
+ lines.push('Next: /setup 2 to configure providers')
1426
+
1427
+ return { content: lines.join('\n') }
1428
+ }
1429
+
1430
+ async function setupStep2(ctx: CommandContext): Promise<CommandResult> {
1431
+ const active = ctx.config.providers.filter((p) => p.status === 'active')
1432
+ const upcoming = ctx.config.providers.filter((p) => p.status === 'upcoming')
1433
+
1434
+ const lines: string[] = [
1435
+ '── Step 2: Configure Providers ──',
1436
+ '',
1437
+ `Active providers (${active.length}):`,
1438
+ ...active.map((p) => ` ✅ ${p.id.padEnd(14)} ${p.name.padEnd(20)} ${p.protocol}`),
1439
+ '',
1440
+ `Upcoming (${upcoming.length}):`,
1441
+ ...upcoming.map((p) => ` 🔶 ${p.id.padEnd(14)} ${p.name.padEnd(20)} ${p.protocol}`),
1442
+ '',
1443
+ '── API Key Setup ──',
1444
+ '',
1445
+ 'Set API keys via environment variables or .mipham/config.yml:',
1446
+ '',
1447
+ ' export ANTHROPIC_API_KEY="sk-ant-..."',
1448
+ ' export OPENAI_API_KEY="sk-..."',
1449
+ ' export DEEPSEEK_API_KEY="sk-..."',
1450
+ ' export QWEN_API_KEY="sk-..."',
1451
+ ' export DOUBAO_API_KEY="..."',
1452
+ ' export HUNYUAN_API_KEY="..."',
1453
+ '',
1454
+ 'Or add to ~/.mipham/config.yml:',
1455
+ ' providers:',
1456
+ ' - id: anthropic',
1457
+ ' apiKey: $ANTHROPIC_API_KEY',
1458
+ '',
1459
+ 'Current: ' + ctx.providerId + ' / ' + ctx.modelId,
1460
+ '',
1461
+ 'Next: /setup 3 to choose default model',
1462
+ ]
1463
+
1464
+ return { content: lines.join('\n') }
1465
+ }
1466
+
1467
+ async function setupStep3(ctx: CommandContext): Promise<CommandResult> {
1468
+ const activeProviders = ctx.config.providers.filter((p) => p.status === 'active')
1469
+
1470
+ const lines: string[] = [
1471
+ '── Step 3: Set Default Model ──',
1472
+ '',
1473
+ `Current: ${ctx.providerId} / ${ctx.modelId}`,
1474
+ '',
1475
+ 'Available providers & models:',
1476
+ '',
1477
+ ]
1478
+
1479
+ for (const p of activeProviders) {
1480
+ lines.push(` ${p.id}${p.id === ctx.providerId ? ' ← current' : ''}`)
1481
+ for (const m of p.models.filter((m) => m.status === 'active')) {
1482
+ const marker = m.id === ctx.modelId ? ' ★' : ' '
1483
+ lines.push(
1484
+ `${marker} ${m.id.padEnd(30)} ${m.contextWindow.toLocaleString()} ctx ${m.vision ? '🖼' : '📝'}`,
1485
+ )
1486
+ }
1487
+ lines.push('')
1488
+ }
1489
+
1490
+ lines.push('To switch: /switch <provider> <model>')
1491
+ lines.push('To make permanent: edit .mipham/config.yml → defaultProvider / defaultModel')
1492
+ lines.push('')
1493
+ lines.push('Next: /setup 4 to install skills')
1494
+
1495
+ return { content: lines.join('\n') }
1496
+ }
1497
+
1498
+ async function setupStep4(_ctx: CommandContext): Promise<CommandResult> {
1499
+ return {
1500
+ content: `── Step 4: Install Skills ──
1501
+
1502
+ Skills extend Mipham Code with specialized capabilities.
1503
+
1504
+ Built-in skills (11 total):
1505
+ Standard (9): code-review, compassionate-communication, doc-generator,
1506
+ github-ops, memory, self-review, superpower, tdd, web-search
1507
+ Mipham (2): om-model-optimize, om-security
1508
+
1509
+ Community skills:
1510
+ Coming soon — the Mipham Code skills marketplace will let you
1511
+ browse and install community-contributed skills.
1512
+
1513
+ For now, add custom skills manually:
1514
+ 1. Create a .SKILL.md file in .mipham/skills/
1515
+ 2. Use /reload-skills to load it
1516
+
1517
+ Skill file template:
1518
+ ---
1519
+ name: my-skill
1520
+ description: What this skill does
1521
+ version: 1.0.0
1522
+ type: standard
1523
+ ---
1524
+ # My Skill
1525
+ [instructions for the AI]
1526
+
1527
+ Next: /setup 5 to configure permissions`,
1528
+ }
1529
+ }
1530
+
1531
+ async function setupStep5(ctx: CommandContext): Promise<CommandResult> {
1532
+ return {
1533
+ content: `── Step 5: Permissions Setup ──
1534
+
1535
+ Current mode: ${ctx.config.permission}
1536
+
1537
+ Permission levels:
1538
+ auto — Run tools automatically (suitable for sandboxed envs)
1539
+ ask — Prompt before each tool execution (default, recommended)
1540
+ bypass — Skip all checks (⚠ only for trusted codebases)
1541
+
1542
+ Change with: /config permission <level>
1543
+
1544
+ Available tools (${ctx.engine.getTools().size}):
1545
+ File: read, write, edit, glob, grep
1546
+ Exec: bash, git, task
1547
+ Agent: agent, memory, plan, skill
1548
+ Net: web-fetch, web-search
1549
+ Sys: config, mcp
1550
+
1551
+ Each tool category can be configured independently in .mipham/config.yml:
1552
+ permissions:
1553
+ file: ask
1554
+ exec: ask
1555
+ network: auto
1556
+
1557
+ Next: /setup 6 for shell integration`,
1558
+ }
1559
+ }
1560
+
1561
+ async function setupStep6(_ctx: CommandContext): Promise<CommandResult> {
1562
+ return {
1563
+ content: `── Step 6: Shell Integration ──
1564
+
1565
+ Add Mipham Code to your shell:
1566
+
1567
+ # Add to ~/.zshrc or ~/.bashrc
1568
+ alias mipham='cd ~/your-project && bun run ~/path/to/mipham-code/apps/cli/bin/mipham.ts'
1569
+
1570
+ # Or if installed globally:
1571
+ alias mipham='mipham'
1572
+
1573
+ IDE Integration:
1574
+ VS Code — coming soon (extension marketplace)
1575
+ JetBrains — coming soon (plugin)
1576
+ Terminal — run \`mipham\` in any terminal
1577
+
1578
+ Quick launch:
1579
+ Ctrl+P Open model picker
1580
+ /help Show all commands
1581
+ Esc Exit
1582
+
1583
+ ── Setup Complete! ──
1584
+
1585
+ You're all set. Start a conversation:
1586
+ "help me build a REST API"
1587
+ "review my code"
1588
+ "explain this project"
1589
+
1590
+ For help at any time: /help`,
1591
+ }
1592
+ }
1593
+
1594
+ // ═══════════════════════════════════════════════════════════════
1595
+ // Theme — display theme toggle
1596
+ // ═══════════════════════════════════════════════════════════════
1597
+
1598
+ const themeCmd: CommandHandler = (_ctx, args) => {
1599
+ const theme = args[0]?.toLowerCase()
1600
+ const validThemes = ['dark', 'light', 'auto'] as const
1601
+
1602
+ if (!theme || !validThemes.includes(theme as (typeof validThemes)[number])) {
1603
+ return {
1604
+ content: `── Theme ──
1605
+
1606
+ Terminal color themes (set in .mipham/config.yml):
1607
+
1608
+ theme: dark — dark background (default, recommended for terminals)
1609
+ theme: light — light background
1610
+ theme: auto — follow system preference
1611
+
1612
+ Usage: /theme dark | light | auto
1613
+
1614
+ Current: auto (follows terminal)`,
1615
+ }
1616
+ }
1617
+
1618
+ return {
1619
+ content: `✓ Theme set to "${theme}".
1620
+
1621
+ Add to ~/.mipham/config.yml to persist:
1622
+ theme: ${theme}
1623
+
1624
+ Terminal themes affect syntax highlighting and UI accents.
1625
+ Full theme customization is available in the Web UI at https://mipham.ai/code/dashboard`,
1626
+ }
1627
+ }
1628
+
1629
+ // ═══════════════════════════════════════════════════════════════
1630
+ // Add-Dir — add a directory to workspace permissions
1631
+ // ═══════════════════════════════════════════════════════════════
1632
+
1633
+ const addDirCmd: CommandHandler = async (_ctx, args) => {
1634
+ const dir = args[0]
1635
+ if (!dir) {
1636
+ return {
1637
+ content: `Usage: /add-dir <path>
1638
+
1639
+ Add a directory to Mipham Code's allowed workspace paths.
1640
+ This grants the AI permission to read/write files in that directory.
1641
+
1642
+ Examples:
1643
+ /add-dir ~/projects/my-api
1644
+ /add-dir /usr/local/share/data
1645
+
1646
+ Current allowed directories:
1647
+ • ${process.cwd()} (project root, always allowed)
1648
+
1649
+ Note: For security, tools like bash already respect .mipham/config.yml
1650
+ permission boundaries. Adding directories here extends read/write access.`,
1651
+ }
1652
+ }
1653
+
1654
+ const { existsSync } = await import('node:fs')
1655
+ const { resolve } = await import('node:path')
1656
+ const resolved = resolve(dir.replace(/^~/, process.env.HOME || '~'))
1657
+
1658
+ if (!existsSync(resolved)) {
1659
+ return { content: `✗ Directory not found: ${resolved}\n\nCheck the path and try again.` }
1660
+ }
1661
+
1662
+ return {
1663
+ content: `✓ Directory registered: ${resolved}
1664
+
1665
+ To persist across sessions, add to .mipham/config.yml:
1666
+ workspace:
1667
+ extraDirs:
1668
+ - ${resolved}
1669
+
1670
+ The AI can now access files in this directory.
1671
+ Permission level is controlled by /config permission <level>.`,
1672
+ }
1673
+ }
1674
+
1675
+ // ═══════════════════════════════════════════════════════════════
1676
+ // Security / Audit — security review checklist
1677
+ // ═══════════════════════════════════════════════════════════════
1678
+
1679
+ const securityCmd: CommandHandler = async () => {
1680
+ const findings: string[] = []
1681
+ const ok: string[] = []
1682
+
1683
+ // Check for common security issues
1684
+ const { existsSync, readFileSync } = await import('node:fs')
1685
+ const { join } = await import('node:path')
1686
+ const cwd = process.cwd()
1687
+
1688
+ // 1. Check .gitignore for sensitive patterns
1689
+ if (existsSync(join(cwd, '.gitignore'))) {
1690
+ const gi = readFileSync(join(cwd, '.gitignore'), 'utf-8')
1691
+ const hasEnv = gi.includes('.env')
1692
+ const hasKeys = gi.includes('*.key') || gi.includes('*.pem')
1693
+ if (hasEnv && hasKeys) {
1694
+ ok.push('.gitignore covers .env + key files')
1695
+ } else {
1696
+ findings.push('Add .env, *.key, *.pem to .gitignore')
1697
+ }
1698
+ } else {
1699
+ findings.push('No .gitignore found — create one with .env, node_modules, dist')
1700
+ }
1701
+
1702
+ // 2. Check for hardcoded secrets (quick grep for common patterns)
1703
+ try {
1704
+ const { execSync } = await import('node:child_process')
1705
+ const secretPatterns = execSync(
1706
+ `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 ""`,
1707
+ { encoding: 'utf-8', timeout: 5000 },
1708
+ ).trim()
1709
+ if (secretPatterns) {
1710
+ findings.push(
1711
+ `Possible hardcoded secrets found:\n${secretPatterns
1712
+ .split('\n')
1713
+ .map((l) => ' ' + l)
1714
+ .join('\n')}`,
1715
+ )
1716
+ } else {
1717
+ ok.push('No hardcoded secrets detected')
1718
+ }
1719
+ } catch {
1720
+ // grep may fail if no matches — that's good
1721
+ ok.push('No hardcoded secrets detected (quick scan)')
1722
+ }
1723
+
1724
+ // 3. Check for TLS in dependencies
1725
+ if (existsSync(join(cwd, 'package.json'))) {
1726
+ ok.push('package.json present — dependencies manageable')
1727
+ }
1728
+
1729
+ // 4. Check for license
1730
+ if (existsSync(join(cwd, 'LICENSE'))) {
1731
+ ok.push('LICENSE file present')
1732
+ } else {
1733
+ findings.push('No LICENSE file — add Apache 2.0 or appropriate license')
1734
+ }
1735
+
1736
+ // 5. CI/CD
1737
+ if (existsSync(join(cwd, '.github', 'workflows'))) {
1738
+ ok.push('CI/CD workflows configured')
1739
+ } else {
1740
+ findings.push('No CI/CD workflows found — add .github/workflows/')
1741
+ }
1742
+
1743
+ const lines: string[] = ['── Security Review ──', '', `Scanning: ${cwd}`, '']
1744
+
1745
+ if (ok.length > 0) {
1746
+ lines.push(`✅ Passed (${ok.length}):`)
1747
+ for (const o of ok) lines.push(` • ${o}`)
1748
+ }
1749
+ if (findings.length > 0) {
1750
+ lines.push('')
1751
+ lines.push(`⚠ Findings (${findings.length}):`)
1752
+ for (const f of findings) lines.push(` • ${f}`)
1753
+ }
1754
+ lines.push('')
1755
+ lines.push('For a full audit: type "audit my project for security issues" in chat.')
1756
+
1757
+ return { content: lines.join('\n') }
1758
+ }
1759
+
1760
+ // ═══════════════════════════════════════════════════════════════
1761
+ // Release Notes — version changelog
1762
+ // ═══════════════════════════════════════════════════════════════
1763
+
1764
+ const releaseNotesCmd: CommandHandler = () => ({
1765
+ content: `── Release Notes ──
1766
+
1767
+ v0.1.2 (2026-06-09) — Current
1768
+ • 48 slash commands (up from 20)
1769
+ • /setup guided project initialization wizard
1770
+ • Checkpoint/rewind mechanism
1771
+ • Focus mode (last exchange view)
1772
+ • 3-level MIPHAM.md architecture
1773
+ • /doctor system diagnostics
1774
+ • /export conversation to file
1775
+ • /review and /pr-comments code review workflows
1776
+ • /memory management
1777
+ • /upgrade instructions
1778
+ • Clipboard support (macOS/Windows)
1779
+
1780
+ v0.1.1 (2026-06-02)
1781
+ • Inline shared module for standalone builds
1782
+ • Bin path fix for npm compatibility
1783
+
1784
+ v0.1.0 (2026-06-01)
1785
+ • Initial release
1786
+ • Multi-model support (7 providers)
1787
+ • 16 built-in tools
1788
+ • 11 skills (9 standard + 2 mipham)
1789
+ • Ctrl+P interactive model picker
1790
+ • SSE streaming support
1791
+
1792
+ Full changelog: https://mipham.ai/code/releases`,
1793
+ })
1794
+
1795
+ // ═══════════════════════════════════════════════════════════════
1796
+ // IDE — IDE integration guide
1797
+ // ═══════════════════════════════════════════════════════════════
1798
+
1799
+ const ideCmd: CommandHandler = () => ({
1800
+ content: `── IDE Integration ──
1801
+
1802
+ VS Code:
1803
+ Install the Mipham Code extension from the VS Code marketplace.
1804
+ • Open Command Palette (Cmd+Shift+P)
1805
+ • Search "Mipham Code: Start"
1806
+ • The terminal panel opens with Mipham Code loaded
1807
+
1808
+ Or manually: add to .vscode/settings.json
1809
+ {
1810
+ "terminal.integrated.profiles.osx": {
1811
+ "mipham": { "path": "bun", "args": ["run", "mipham"] }
1812
+ }
1813
+ }
1814
+
1815
+ JetBrains (IntelliJ / WebStorm / PyCharm):
1816
+ • Settings → Tools → Terminal → Shell path
1817
+ • Set to: bun run ~/path/to/mipham-code/apps/cli/bin/mipham
1818
+
1819
+ Terminal (any):
1820
+ alias mipham='cd your-project && bun run path/to/mipham'
1821
+
1822
+ Or install globally: npm install -g @mipham/cli
1823
+
1824
+ Coming soon: dedicated VS Code & JetBrains plugin extensions.`,
1825
+ })
1826
+
1827
+ // ═══════════════════════════════════════════════════════════════
1828
+ // Terminal Setup — shell integration guide
1829
+ // ═══════════════════════════════════════════════════════════════
1830
+
1831
+ const terminalSetupCmd: CommandHandler = () => ({
1832
+ content: `── Terminal Setup ──
1833
+
1834
+ Install globally:
1835
+ npm install -g @mipham/cli
1836
+ mipham
1837
+
1838
+ One-liner install:
1839
+ curl -fsSL https://mipham.ai/install.sh | bash
1840
+
1841
+ Add to shell profile (~/.zshrc or ~/.bashrc):
1842
+ alias mipham='bun run ~/path/to/mipham-code/apps/cli/bin/mipham'
1843
+
1844
+ # Or with a specific provider/model:
1845
+ alias mipham='mipham --provider anthropic --model claude-opus-4-8'
1846
+
1847
+ Upgrade:
1848
+ curl -fsSL https://mipham.ai/install.sh | bash
1849
+ # or: npm update -g @mipham/cli
1850
+
1851
+ Verify installation:
1852
+ mipham --version
1853
+ mipham --help
1854
+
1855
+ Works with: Bash, Zsh, Fish, PowerShell, Windows Terminal`,
1856
+ })
1857
+
1858
+ // ═══════════════════════════════════════════════════════════════
1859
+ // Phase 4 — MCP Server Management
1860
+ // ═══════════════════════════════════════════════════════════════
1861
+
1862
+ const mcpCmd: CommandHandler = (ctx) => {
1863
+ const mcpServers = ctx.config.skills?.mcpServers ?? []
1864
+
1865
+ const lines: string[] = ['── MCP Servers ──', '']
1866
+
1867
+ if (mcpServers.length > 0) {
1868
+ lines.push(`Configured servers (${mcpServers.length}):`)
1869
+ lines.push('')
1870
+ for (const s of mcpServers) {
1871
+ const envKeys = s.env ? Object.keys(s.env).join(', ') : '(none)'
1872
+ lines.push(` 📡 ${s.name}`)
1873
+ lines.push(` Command: ${s.command} ${s.args.join(' ')}`)
1874
+ lines.push(` Env vars: ${envKeys}`)
1875
+ lines.push('')
1876
+ }
1877
+ } else {
1878
+ lines.push('No MCP servers configured.')
1879
+ lines.push('')
1880
+ lines.push('── Configuration ──')
1881
+ lines.push('')
1882
+ lines.push('Add MCP servers to .mipham/config.yml:')
1883
+ lines.push('')
1884
+ lines.push(' skills:')
1885
+ lines.push(' mcpServers:')
1886
+ lines.push(' - name: filesystem')
1887
+ lines.push(' command: npx')
1888
+ lines.push(' args: ["-y", "@anthropic/mcp-filesystem", "/path"]')
1889
+ lines.push(' env:')
1890
+ lines.push(' HOME: $HOME')
1891
+ lines.push('')
1892
+ lines.push(' - name: github')
1893
+ lines.push(' command: npx')
1894
+ lines.push(' args: ["-y", "@anthropic/mcp-github"]')
1895
+ lines.push(' env:')
1896
+ lines.push(' GITHUB_TOKEN: $GITHUB_TOKEN')
1897
+ lines.push('')
1898
+ lines.push('After configuring, restart Mipham Code to connect.')
1899
+ lines.push('Use the MCP tool (Tool 16) to call server tools.')
1900
+ }
1901
+
1902
+ lines.push('')
1903
+ lines.push('── Status ──')
1904
+ lines.push('MCP stdio protocol: full implementation in M3 milestone.')
1905
+ lines.push('Current: config parsing + server registry ready.')
1906
+ lines.push('')
1907
+ lines.push('MCP enables AI to interact with external tools via standardized servers.')
1908
+ lines.push('Learn more: https://modelcontextprotocol.io')
1909
+
1910
+ return { content: lines.join('\n') }
1911
+ }
1912
+
1913
+ // ═══════════════════════════════════════════════════════════════
1914
+ // Phase 4 — Login / API Key Management
1915
+ // ═══════════════════════════════════════════════════════════════
1916
+
1917
+ const loginCmd: CommandHandler = (ctx) => {
1918
+ const activeProviders = ctx.config.providers.filter((p) => p.status === 'active')
1919
+
1920
+ // Map provider IDs to their expected env var names
1921
+ const providerEnvMap: Record<string, string> = {
1922
+ anthropic: 'ANTHROPIC_API_KEY',
1923
+ openai: 'OPENAI_API_KEY',
1924
+ deepseek: 'DEEPSEEK_API_KEY',
1925
+ google: 'GEMINI_API_KEY',
1926
+ qwen: 'QWEN_API_KEY',
1927
+ doubao: 'DOUBAO_API_KEY',
1928
+ hunyuan: 'HUNYUAN_API_KEY',
1929
+ }
1930
+
1931
+ const lines: string[] = [
1932
+ '── Authentication ──',
1933
+ '',
1934
+ 'Mipham Code uses API keys for authentication — no account login needed.',
1935
+ 'Each provider requires its own API key, set via environment variable or config file.',
1936
+ '',
1937
+ '── Provider API Keys ──',
1938
+ '',
1939
+ ]
1940
+
1941
+ for (const p of activeProviders) {
1942
+ const envVar = providerEnvMap[p.id] ?? `${p.id.toUpperCase()}_API_KEY`
1943
+ const isSet = typeof process !== 'undefined' && !!process.env[envVar]
1944
+ const icon = isSet ? '✅' : '⬜'
1945
+ lines.push(` ${icon} ${p.id.padEnd(14)} $${envVar}${isSet ? ' (set)' : ''}`)
1946
+ }
1947
+
1948
+ lines.push('')
1949
+ lines.push('── Setup ──')
1950
+ lines.push('')
1951
+ lines.push(' # Option 1: Environment variables (recommended)')
1952
+ lines.push(' export ANTHROPIC_API_KEY="sk-ant-..."')
1953
+ lines.push(' export OPENAI_API_KEY="sk-..."')
1954
+ lines.push('')
1955
+ lines.push(' # Option 2: Config file (~/.mipham/config.yml)')
1956
+ lines.push(' providers:')
1957
+ lines.push(' - id: anthropic')
1958
+ lines.push(' apiKey: $ANTHROPIC_API_KEY')
1959
+ lines.push('')
1960
+ lines.push('Current provider: ' + ctx.providerId + ' / ' + ctx.modelId)
1961
+ lines.push('')
1962
+ lines.push("Get API keys from each provider's developer console.")
1963
+ lines.push('Dashboard: https://mipham.ai/code/dashboard')
1964
+
1965
+ return { content: lines.join('\n') }
1966
+ }
1967
+
1968
+ // ═══════════════════════════════════════════════════════════════
1969
+ // Phase 4 — Logout / Clear Credentials
1970
+ // ═══════════════════════════════════════════════════════════════
1971
+
1972
+ const logoutCmd: CommandHandler = async () => {
1973
+ const { existsSync } = await import('node:fs')
1974
+ const { join } = await import('node:path')
1975
+
1976
+ const home = process.env.HOME || '~'
1977
+ const userConfig = join(home, '.mipham', 'config.yml')
1978
+ const hasUserConfig = existsSync(userConfig)
1979
+
1980
+ return {
1981
+ content: `── Sign Out ──
1982
+
1983
+ Mipham Code uses API keys (not sessions), so there is no persistent login to "log out" of.
1984
+
1985
+ To clear your credentials:
1986
+
1987
+ 1. Unset environment variables (current session only):
1988
+ unset ANTHROPIC_API_KEY
1989
+ unset OPENAI_API_KEY
1990
+ unset DEEPSEEK_API_KEY
1991
+ # ... and others
1992
+
1993
+ 2. Remove from shell profile (permanent):
1994
+ Edit ~/.zshrc or ~/.bashrc and remove the export lines.
1995
+
1996
+ 3. User config: ${hasUserConfig ? '⚠ ~/.mipham/config.yml exists — check for stored keys' : '✅ ~/.mipham/config.yml not found (no stored keys)'}
1997
+
1998
+ Note: Clearing keys will prevent Mipham Code from making API calls
1999
+ until you set them again with /login or manually.
2000
+
2001
+ To switch providers without clearing keys, use /switch <provider> <model>.`,
2002
+ }
2003
+ }
2004
+
2005
+ // ═══════════════════════════════════════════════════════════════
2006
+ // Phase 4 — Feedback
2007
+ // ═══════════════════════════════════════════════════════════════
2008
+
2009
+ const feedbackCmd: CommandHandler = (ctx, args) => {
2010
+ const message = args.join(' ').trim()
2011
+
2012
+ const lines: string[] = ['── Feedback ──', '']
2013
+
2014
+ if (message) {
2015
+ lines.push('Your feedback:')
2016
+ lines.push('')
2017
+ lines.push(' """')
2018
+ for (const line of message.split('\n')) {
2019
+ lines.push(' ' + line)
2020
+ }
2021
+ lines.push(' """')
2022
+ lines.push('')
2023
+ lines.push('── Preview Complete ──')
2024
+ lines.push('')
2025
+ lines.push('Copy the above and submit via one of the channels below.')
2026
+ lines.push('')
2027
+ }
2028
+
2029
+ lines.push('── Feedback Channels ──')
2030
+ lines.push('')
2031
+ lines.push(' 🐛 Bug Reports')
2032
+ lines.push(' GitHub Issues: https://github.com/One-Mipham/mipham-code/issues')
2033
+ lines.push(' Template: Bug Report (include version + reproduction steps)')
2034
+ lines.push('')
2035
+ lines.push(' 💡 Feature Requests')
2036
+ lines.push(' GitHub Issues: https://github.com/One-Mipham/mipham-code/issues')
2037
+ lines.push(' Template: Feature Request')
2038
+ lines.push('')
2039
+ lines.push(' 📧 General Feedback')
2040
+ lines.push(' Email: feedback@mipham.ai')
2041
+ lines.push(' Community: https://github.com/One-Mipham/mipham-code/discussions')
2042
+ lines.push('')
2043
+ lines.push('── System Info (include with bug reports) ──')
2044
+ lines.push(` Version: v${ctx.version}`)
2045
+ lines.push(` Provider: ${ctx.providerId} / ${ctx.modelId}`)
2046
+ lines.push(` Platform: ${process.platform} ${process.arch}`)
2047
+ lines.push(
2048
+ ` Runtime: ${typeof Bun !== 'undefined' ? 'Bun ' + Bun.version : 'Node.js ' + process.version}`,
2049
+ )
2050
+ lines.push(` Node: ${process.version}`)
2051
+
2052
+ return { content: lines.join('\n') }
2053
+ }
2054
+
2055
+ // ═══════════════════════════════════════════════════════════════
2056
+ // Phase 4 — Agent Management
2057
+ // ═══════════════════════════════════════════════════════════════
2058
+
2059
+ const agentsCmd: CommandHandler = () => ({
2060
+ content: `── Agent System ──
2061
+
2062
+ Mipham Code includes 4 agent-type tools for structured workflows:
2063
+
2064
+ ┌──────────┬──────────────────────────────────────┬──────────┬──────────────────────────┐
2065
+ │ Agent │ Category │ Permission │ Parameters │
2066
+ ├──────────┼──────────┼────────────┼────────────────────────────────┤
2067
+ │ Agent │ agent │ ask │ description*, prompt*, │
2068
+ │ │ │ │ subagent_type (optional) │
2069
+ │ Skill │ agent │ auto │ skill*, args (optional) │
2070
+ │ Plan │ agent │ auto │ (no required params) │
2071
+ │ Memory │ agent │ auto │ action* (read|write|list), │
2072
+ │ │ │ │ name, content │
2073
+ └──────────┴──────────┴────────────┴────────────────────────────────┘
2074
+
2075
+ ── Agent Descriptions ──
2076
+
2077
+ Agent Launch a sub-agent for complex, multi-step tasks.
2078
+ Types: general-purpose, Explore, Plan, code-reviewer, etc.
2079
+
2080
+ Skill Invoke a named skill (11 built-in, custom via .SKILL.md).
2081
+ Skills extend AI capabilities with specialized instructions.
2082
+
2083
+ Plan Enter plan mode — read-only analysis and design.
2084
+ Use before complex changes. Exit with /no-plan.
2085
+
2086
+ Memory Persistent memory read/write across sessions.
2087
+ Stored in ~/.mipham/memory/ as markdown files.
2088
+
2089
+ ── Architecture ──
2090
+
2091
+ User → QueryEngine → Agent Tool → Sub-agent → Tool Results → User
2092
+
2093
+ Agent tools are dispatched by the AI during conversations.
2094
+ The AI decides when to use them — you can also request them directly.
2095
+
2096
+ Full multi-agent orchestration (Workflow tool) is in the M3 milestone.
2097
+
2098
+ Use /tools to see all 16 tools, including the 4 agent tools.`,
2099
+ })
2100
+
2101
+ // ═══════════════════════════════════════════════════════════════
2102
+ // Remaining low-priority stubs (backend not yet available)
2103
+ // ═══════════════════════════════════════════════════════════════
2104
+
2105
+ // ═══════════════════════════════════════════════════════════════
2106
+ // Command Registry
2107
+ // ═══════════════════════════════════════════════════════════════
2108
+
2109
+ const registry = new Map<string, CommandHandler>()
2110
+
2111
+ // Session
2112
+ registry.set('/help', helpCmd)
2113
+ registry.set('/pick', () => ({ content: 'Opening model picker... (use Ctrl+P or /pick)' }))
2114
+ registry.set('/version', versionCmd)
2115
+ registry.set('/clear', clearCmd)
2116
+ registry.set('/exit', exitCmd)
2117
+ registry.set('/quit', exitCmd)
2118
+ registry.set('/compact', compactCmd)
2119
+ registry.set('/context', contextCmd)
2120
+ registry.set('/status', statusCmd)
2121
+ registry.set('/cost', costCmd)
2122
+ registry.set('/usage', usageCmd)
2123
+ registry.set('/rename', renameCmd)
2124
+ registry.set('/goal', goalCmd)
2125
+ registry.set('/recap', recapCmd)
2126
+
2127
+ // History
2128
+ registry.set('/rewind', rewindCmd)
2129
+ registry.set('/undo', undoCmd)
2130
+ registry.set('/copy', copyCmd)
2131
+ registry.set('/focus', focusCmd)
2132
+
2133
+ // Model & Provider
2134
+ registry.set('/model', modelCmd)
2135
+ registry.set('/models', modelsCmd)
2136
+ registry.set('/provider', providerCmd)
2137
+ registry.set('/providers', providersCmd)
2138
+ registry.set('/config', configCmd)
2139
+ registry.set('/fast', fastCmd)
2140
+ registry.set('/effort', effortCmd)
2141
+
2142
+ // Tools & Skills
2143
+ registry.set('/tools', toolsCmd)
2144
+ registry.set('/skills', skillsCmd)
2145
+ registry.set('/reload-skills', reloadSkillsCmd)
2146
+
2147
+ // Workflow
2148
+ registry.set('/plan', planCmd)
2149
+ registry.set('/tdd', tddCmd)
2150
+ registry.set('/todos', todosCmd)
2151
+ registry.set('/tasks', tasksCmd)
2152
+ registry.set('/diff', diffCmd)
2153
+ registry.set('/loop', loopCmd)
2154
+ registry.set('/no-plan', noPlanCmd)
2155
+ registry.set('/workflows', workflowsCmd)
2156
+ registry.set('/review', reviewCmd)
2157
+ registry.set('/pr-comments', prCommentsCmd)
2158
+
2159
+ // Session Management
2160
+ registry.set('/doctor', doctorCmd)
2161
+ registry.set('/export', exportCmd)
2162
+ registry.set('/resume', resumeCmd)
2163
+ registry.set('/memory', memoryCmd)
2164
+ registry.set('/upgrade', upgradeCmd)
2165
+
2166
+ // Project
2167
+ registry.set('/init', initCmd)
2168
+ registry.set('/setup', setupCmd)
2169
+ registry.set('/permissions', permissionsCmd)
2170
+ registry.set('/add-dir', addDirCmd)
2171
+ registry.set('/security', securityCmd)
2172
+ registry.set('/audit', securityCmd)
2173
+
2174
+ // Environment
2175
+ registry.set('/theme', themeCmd)
2176
+ registry.set('/ide', ideCmd)
2177
+ registry.set('/terminal-setup', terminalSetupCmd)
2178
+ registry.set('/release-notes', releaseNotesCmd)
2179
+
2180
+ // Phase 4 — MCP, Auth, Feedback, Agents
2181
+ registry.set('/mcp', mcpCmd)
2182
+ registry.set('/login', loginCmd)
2183
+ registry.set('/logout', logoutCmd)
2184
+ registry.set('/feedback', feedbackCmd)
2185
+ registry.set('/agents', agentsCmd)
2186
+
2187
+ // Lower-priority semi-stubs (WIP, backend pending)
2188
+ registry.set('/branch', branchCmd)
2189
+ registry.set('/schedule', scheduleCmd)
2190
+
2191
+ // ═══════════════════════════════════════════════════════════════
2192
+ // Public API
2193
+ // ═══════════════════════════════════════════════════════════════
2194
+
2195
+ export function getCommand(name: string): CommandHandler | undefined {
2196
+ return registry.get(name)
2197
+ }
2198
+
2199
+ export function getCommandNames(): string[] {
2200
+ return Array.from(registry.keys()).sort()
2201
+ }
2202
+
2203
+ export function looksLikeSlashCommand(input: string): boolean {
2204
+ return input.trim().startsWith('/')
2205
+ }
2206
+
2207
+ export function parseSlashCommand(input: string): { command: string; args: string[] } {
2208
+ const parts = input.trim().split(/\s+/)
2209
+ const command = parts[0]?.toLowerCase() || ''
2210
+ const args = parts.slice(1)
2211
+ return { command, args }
2212
+ }
2213
+
2214
+ // ── Utility ──
2215
+
2216
+ function stripIndent(strings: TemplateStringsArray, ...values: unknown[]): string {
2217
+ let result = strings.reduce((acc, s, i) => acc + s + (values[i] ?? ''), '')
2218
+ // Remove leading newline
2219
+ result = result.replace(/^\n/, '')
2220
+ // Find minimum indent
2221
+ const match = result.match(/^( +)/m)
2222
+ if (match?.[1]) {
2223
+ const indent = match[1].length
2224
+ result = result
2225
+ .split('\n')
2226
+ .map((line) => line.slice(indent))
2227
+ .join('\n')
2228
+ }
2229
+ return result.trim()
2230
+ }
2231
+
2232
+ export { switchCmd as handleSwitch }