@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.
- package/README.md +76 -0
- package/assets/icon.icns +0 -0
- package/assets/icon.jpg +0 -0
- package/bin/mipham +51 -0
- package/bin/mipham.ts +8 -0
- package/package.json +59 -0
- package/skills/mipham/om-model-optimize.mipham-skill.md +20 -0
- package/skills/mipham/om-security.mipham-skill.md +21 -0
- package/skills/standard/code-review.SKILL.md +116 -0
- package/skills/standard/compassionate-communication.SKILL.md +80 -0
- package/skills/standard/doc-generator.SKILL.md +21 -0
- package/skills/standard/github-ops.SKILL.md +22 -0
- package/skills/standard/memory.SKILL.md +22 -0
- package/skills/standard/mipham-code-setup.SKILL.md +113 -0
- package/skills/standard/security-review.SKILL.md +122 -0
- package/skills/standard/self-review.SKILL.md +20 -0
- package/skills/standard/superpower.SKILL.md +19 -0
- package/skills/standard/tdd.SKILL.md +20 -0
- package/skills/standard/web-search.SKILL.md +23 -0
- package/src/agent/sub-agent.ts +122 -0
- package/src/config/defaults.ts +10 -0
- package/src/config/loader.ts +30 -0
- package/src/core/context.ts +135 -0
- package/src/core/engine.ts +193 -0
- package/src/core/hooks.ts +116 -0
- package/src/core/instructions.ts +126 -0
- package/src/core/permission.ts +54 -0
- package/src/core/session-store.ts +136 -0
- package/src/index.tsx +93 -0
- package/src/mcp/client.ts +170 -0
- package/src/mcp/protocol.ts +104 -0
- package/src/mcp/transport.ts +169 -0
- package/src/mcp/types.ts +112 -0
- package/src/providers/anthropic.ts +286 -0
- package/src/providers/bootstrap.ts +28 -0
- package/src/providers/openai-compat.ts +158 -0
- package/src/providers/registry.ts +70 -0
- package/src/security/path.ts +90 -0
- package/src/security/url.ts +96 -0
- package/src/shared/constants.ts +368 -0
- package/src/shared/index.ts +2 -0
- package/src/shared/types.ts +161 -0
- package/src/skills/loader.ts +109 -0
- package/src/skills/mipham/runtime.ts +63 -0
- package/src/skills/standard/runtime.ts +59 -0
- package/src/tools/agent/agent.ts +51 -0
- package/src/tools/agent/memory.ts +51 -0
- package/src/tools/agent/plan.ts +87 -0
- package/src/tools/agent/skill.ts +58 -0
- package/src/tools/exec/bash.ts +111 -0
- package/src/tools/exec/git.ts +63 -0
- package/src/tools/exec/task.ts +69 -0
- package/src/tools/file/edit.ts +57 -0
- package/src/tools/file/glob.ts +29 -0
- package/src/tools/file/grep.ts +52 -0
- package/src/tools/file/read.ts +38 -0
- package/src/tools/file/write.ts +27 -0
- package/src/tools/index.ts +126 -0
- package/src/tools/network/web-fetch.ts +61 -0
- package/src/tools/network/web-search.ts +32 -0
- package/src/tools/system/config.ts +68 -0
- package/src/tools/system/mcp.ts +75 -0
- package/src/ui/app.tsx +317 -0
- package/src/ui/chat.tsx +76 -0
- package/src/ui/commands.ts +2232 -0
- package/src/ui/input.tsx +83 -0
- 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 }
|