@miphamai/cli 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/mipham/self-audit.mipham-skill.md +171 -0
- package/src/core/auto-memory.ts +613 -0
- package/src/core/crsi-sandbox.ts +546 -0
- package/src/core/engine.ts +20 -0
- package/src/core/permission.ts +65 -10
- package/src/tools/agent/enter-plan.ts +5 -5
- package/src/tools/agent/exit-plan.ts +36 -27
- package/src/tools/exec/bash.ts +113 -0
- package/src/ui/input.tsx +61 -8
|
@@ -1,53 +1,62 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
1
3
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
4
|
|
|
3
5
|
export const exitPlanModeTool: ToolDefinition = {
|
|
4
6
|
name: 'ExitPlanMode',
|
|
5
7
|
description:
|
|
6
|
-
'Exit plan mode and
|
|
7
|
-
'
|
|
8
|
-
'
|
|
9
|
-
'
|
|
8
|
+
'Exit plan mode and present your plan for user approval. ' +
|
|
9
|
+
'This tool does NOT switch to implementation mode — the user must explicitly approve first. ' +
|
|
10
|
+
'After calling this, present your plan and ask the user to confirm. ' +
|
|
11
|
+
'The user can approve by saying "approved" or "/approve", or by cycling to acceptEdits mode with Shift+Tab.',
|
|
10
12
|
category: 'agent',
|
|
11
13
|
permission: 'auto',
|
|
12
14
|
parameters: {
|
|
13
15
|
type: 'object',
|
|
14
16
|
properties: {
|
|
15
|
-
|
|
16
|
-
type: '
|
|
17
|
+
planFile: {
|
|
18
|
+
type: 'string',
|
|
17
19
|
description:
|
|
18
|
-
'
|
|
20
|
+
'Path to the plan file you wrote (e.g., .mipham/plans/plan-2026-08-10T12-00-00.md). If omitted, the most recent plan file is used.',
|
|
19
21
|
},
|
|
20
22
|
},
|
|
21
|
-
required: [
|
|
23
|
+
required: [],
|
|
22
24
|
},
|
|
23
|
-
async execute(params,
|
|
24
|
-
const
|
|
25
|
+
async execute(params, ctx) {
|
|
26
|
+
const planDir = join(ctx.cwd, '.mipham', 'plans')
|
|
27
|
+
const planFile = (params.planFile as string) || ''
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'✓ Exiting plan mode.',
|
|
33
|
-
'✓ Switching to acceptEdits mode — reads and file edits are auto-approved.',
|
|
34
|
-
'',
|
|
35
|
-
'You can now implement the plan. The plan file is in .mipham/plans/.',
|
|
36
|
-
'',
|
|
37
|
-
'Use Shift+Tab to cycle permission modes if you need to change.',
|
|
38
|
-
].join('\n'),
|
|
29
|
+
// Try to read the plan to confirm it exists
|
|
30
|
+
let planContent = ''
|
|
31
|
+
try {
|
|
32
|
+
const planPath = planFile || ''
|
|
33
|
+
if (planPath) {
|
|
34
|
+
planContent = readFileSync(planPath, 'utf-8')
|
|
39
35
|
}
|
|
36
|
+
} catch {
|
|
37
|
+
// Plan file not found — still exit plan mode
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
return {
|
|
43
41
|
success: true,
|
|
44
42
|
content: [
|
|
45
|
-
'── Plan
|
|
43
|
+
'── Plan Ready for Review ──',
|
|
46
44
|
'',
|
|
47
|
-
'✓
|
|
45
|
+
'✓ Exiting plan mode.',
|
|
46
|
+
'✓ Plan file saved. Present your plan to the user now.',
|
|
48
47
|
'',
|
|
49
|
-
'
|
|
50
|
-
'
|
|
48
|
+
'⚠️ IMPORTANT: You are still in limited permission mode.',
|
|
49
|
+
' The user must explicitly approve before you can make changes.',
|
|
50
|
+
'',
|
|
51
|
+
'Next steps:',
|
|
52
|
+
' 1. Present your plan to the user (summarize key decisions)',
|
|
53
|
+
' 2. Ask: "Does this plan look good? Reply approved to begin."',
|
|
54
|
+
' 3. Wait for the user to explicitly say "approved" or "/approve"',
|
|
55
|
+
' 4. Only then switch to acceptEdits mode (Shift+Tab or user action)',
|
|
56
|
+
'',
|
|
57
|
+
'DO NOT start implementing until the user explicitly approves.',
|
|
58
|
+
'DO NOT call ExitPlanMode with approved:true — that parameter no longer exists.',
|
|
59
|
+
planContent ? `\n── Plan Content (for reference) ──\n\n${planContent.slice(0, 3000)}` : '',
|
|
51
60
|
].join('\n'),
|
|
52
61
|
}
|
|
53
62
|
},
|
package/src/tools/exec/bash.ts
CHANGED
|
@@ -185,6 +185,105 @@ export function detectViolations(stderr: string): string[] {
|
|
|
185
185
|
return violations
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Vibe coding: Parse stderr output for error locations (file path + line + column).
|
|
190
|
+
* Supports common tool formats: TypeScript, ESLint, pytest, Rust, Go, Prettier, etc.
|
|
191
|
+
* Returns up to 10 unique locations sorted by file then line.
|
|
192
|
+
*/
|
|
193
|
+
interface ErrorLocation {
|
|
194
|
+
file: string
|
|
195
|
+
line: number
|
|
196
|
+
col?: number
|
|
197
|
+
message?: string
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function parseErrorLocations(stderr: string): ErrorLocation[] {
|
|
201
|
+
const locations: ErrorLocation[] = []
|
|
202
|
+
|
|
203
|
+
// TypeScript / ESLint / Prettier: path(line,col): message
|
|
204
|
+
// e.g., src/foo.ts(42,10): error TS2304: Cannot find name 'foo'
|
|
205
|
+
const tsPattern = /([^\s(]+)\((\d+),(\d+)\):\s*(.+)/g
|
|
206
|
+
let match: RegExpExecArray | null
|
|
207
|
+
let m: RegExpExecArray | null
|
|
208
|
+
while ((m = tsPattern.exec(stderr)) !== null) {
|
|
209
|
+
locations.push({
|
|
210
|
+
file: m[1]!,
|
|
211
|
+
line: parseInt(m[2]!),
|
|
212
|
+
col: parseInt(m[3]!),
|
|
213
|
+
message: (m[4] || '').slice(0, 120),
|
|
214
|
+
})
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// pytest / Python: path:line: message
|
|
218
|
+
// e.g., tests/test_foo.py:42: AssertionError: ...
|
|
219
|
+
const pyPattern = /([^\s:]+\.py):(\d+):\s*(.+)/g
|
|
220
|
+
while ((match = pyPattern.exec(stderr)) !== null) {
|
|
221
|
+
const pm = match
|
|
222
|
+
locations.push({
|
|
223
|
+
file: pm[1]!,
|
|
224
|
+
line: parseInt(pm[2]!),
|
|
225
|
+
message: (pm[3] || '').slice(0, 120),
|
|
226
|
+
})
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Rust: --> path:line:col
|
|
230
|
+
// e.g., --> src/main.rs:42:10
|
|
231
|
+
const rustPattern = /-->\s*([^\s:]+):(\d+):(\d+)/g
|
|
232
|
+
while ((match = rustPattern.exec(stderr)) !== null) {
|
|
233
|
+
const rm = match
|
|
234
|
+
locations.push({
|
|
235
|
+
file: rm[1]!,
|
|
236
|
+
line: parseInt(rm[2]!),
|
|
237
|
+
col: parseInt(rm[3]!),
|
|
238
|
+
})
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Go: path:line:col: message
|
|
242
|
+
// e.g., ./main.go:42:10: undefined: foo
|
|
243
|
+
const goPattern = /([^\s:]+\.go):(\d+):(\d+):\s*(.+)/g
|
|
244
|
+
while ((match = goPattern.exec(stderr)) !== null) {
|
|
245
|
+
const gm2 = match
|
|
246
|
+
locations.push({
|
|
247
|
+
file: gm2[1]!,
|
|
248
|
+
line: parseInt(gm2[2]!),
|
|
249
|
+
col: parseInt(gm2[3]!),
|
|
250
|
+
message: (gm2[4] || '').slice(0, 120),
|
|
251
|
+
})
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Generic: path:line (any file extension)
|
|
255
|
+
// e.g., src/foo.ts:42
|
|
256
|
+
const genericPattern = /([^\s:]+\.[a-zA-Z]{1,6}):(\d+)\b/g
|
|
257
|
+
while ((match = genericPattern.exec(stderr)) !== null) {
|
|
258
|
+
const gm = match
|
|
259
|
+
const file = gm[1]!
|
|
260
|
+
// Skip if we already have this exact location from a more specific pattern
|
|
261
|
+
const alreadyHave = locations.some((l) => l.file === file && l.line === parseInt(gm[2]!))
|
|
262
|
+
if (!alreadyHave) {
|
|
263
|
+
locations.push({ file, line: parseInt(match[2]!) })
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Deduplicate and sort: same file+line → keep first
|
|
268
|
+
const seen = new Set<string>()
|
|
269
|
+
const unique: ErrorLocation[] = []
|
|
270
|
+
for (const loc of locations) {
|
|
271
|
+
const key = `${loc.file}:${loc.line}`
|
|
272
|
+
if (!seen.has(key)) {
|
|
273
|
+
seen.add(key)
|
|
274
|
+
unique.push(loc)
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Sort by file path then line number
|
|
279
|
+
unique.sort((a, b) => {
|
|
280
|
+
const fileCmp = a.file.localeCompare(b.file)
|
|
281
|
+
return fileCmp !== 0 ? fileCmp : a.line - b.line
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
return unique.slice(0, 10)
|
|
285
|
+
}
|
|
286
|
+
|
|
188
287
|
export const bashTool: ToolDefinition = {
|
|
189
288
|
name: 'Bash',
|
|
190
289
|
description:
|
|
@@ -283,6 +382,20 @@ export const bashTool: ToolDefinition = {
|
|
|
283
382
|
if (violations.length > 0) {
|
|
284
383
|
errorContent += '\n\n── Sandbox Violations ──\n' + violations.join('\n')
|
|
285
384
|
}
|
|
385
|
+
// Vibe coding fix: auto-parse error locations from stderr
|
|
386
|
+
const errorLocations = parseErrorLocations(rawStderr)
|
|
387
|
+
if (errorLocations.length > 0) {
|
|
388
|
+
errorContent +=
|
|
389
|
+
'\n\n── Error Locations (for quick fix) ──\n' +
|
|
390
|
+
errorLocations
|
|
391
|
+
.map(
|
|
392
|
+
(l) =>
|
|
393
|
+
` ${l.file}:${l.line}` +
|
|
394
|
+
(l.col ? `:${l.col}` : '') +
|
|
395
|
+
(l.message ? ` — ${l.message}` : ''),
|
|
396
|
+
)
|
|
397
|
+
.join('\n')
|
|
398
|
+
}
|
|
286
399
|
return {
|
|
287
400
|
success: false,
|
|
288
401
|
content: errorContent,
|
package/src/ui/input.tsx
CHANGED
|
@@ -83,6 +83,16 @@ export function InputBar({
|
|
|
83
83
|
const [completionVerb, setCompletionVerb] = useState<string | null>(null)
|
|
84
84
|
const prevLoading = useRef(isLoading)
|
|
85
85
|
|
|
86
|
+
// ── Message history for arrow-key navigation (Claude Code parity) ──
|
|
87
|
+
const [submittedHistory, setSubmittedHistory] = useState<string[]>([])
|
|
88
|
+
const historyIndexRef = useRef(-1) // -1 = not browsing history
|
|
89
|
+
const savedDraftRef = useRef('') // saved user draft before browsing history
|
|
90
|
+
|
|
91
|
+
// Stabilize t ref — prevents stale closures in intervals and avoids
|
|
92
|
+
// unnecessary effect re-runs when the i18n context value object changes.
|
|
93
|
+
const tRef = useRef(t)
|
|
94
|
+
tRef.current = t
|
|
95
|
+
|
|
86
96
|
// ── Slash command hints ──
|
|
87
97
|
const allCommands = useMemo(() => getCommandList(), [])
|
|
88
98
|
const slashHints = useMemo(() => {
|
|
@@ -92,32 +102,33 @@ export function InputBar({
|
|
|
92
102
|
return allCommands.filter((c) => c.name.toLowerCase().includes(filter)).slice(0, 8)
|
|
93
103
|
}, [value, allCommands])
|
|
94
104
|
|
|
95
|
-
// Rotate gerunds while loading
|
|
105
|
+
// Rotate gerunds while loading — use tRef to avoid re-running when i18n context re-renders
|
|
96
106
|
useEffect(() => {
|
|
97
107
|
if (!isLoading) return
|
|
98
108
|
const interval = setInterval(() => {
|
|
99
|
-
setVerb(
|
|
100
|
-
},
|
|
109
|
+
setVerb(tRef.current(pick(LOADING_KEYS)))
|
|
110
|
+
}, 2000)
|
|
101
111
|
return () => clearInterval(interval)
|
|
102
|
-
}, [isLoading
|
|
112
|
+
}, [isLoading])
|
|
103
113
|
|
|
104
114
|
// Pick a fresh gerund when loading starts
|
|
105
115
|
useEffect(() => {
|
|
106
116
|
if (isLoading) {
|
|
107
|
-
setVerb(
|
|
117
|
+
setVerb(tRef.current(pick(LOADING_KEYS)))
|
|
108
118
|
setCompletionVerb(null)
|
|
109
119
|
}
|
|
110
|
-
}, [isLoading
|
|
120
|
+
}, [isLoading])
|
|
111
121
|
|
|
112
122
|
// Flash a past participle when loading stops
|
|
113
123
|
useEffect(() => {
|
|
114
124
|
if (prevLoading.current === true && isLoading === false) {
|
|
115
|
-
setCompletionVerb(
|
|
125
|
+
setCompletionVerb(tRef.current(pick(COMPLETED_KEYS)))
|
|
116
126
|
const timer = setTimeout(() => setCompletionVerb(null), 1500)
|
|
127
|
+
prevLoading.current = isLoading
|
|
117
128
|
return () => clearTimeout(timer)
|
|
118
129
|
}
|
|
119
130
|
prevLoading.current = isLoading
|
|
120
|
-
}, [isLoading
|
|
131
|
+
}, [isLoading])
|
|
121
132
|
|
|
122
133
|
const [vimMode, setVimMode] = useState<VimMode>('insert')
|
|
123
134
|
// NOTE: Dual mode state — React state (vimMode) drives UI re-renders (prompt color,
|
|
@@ -195,6 +206,39 @@ export function InputBar({
|
|
|
195
206
|
return
|
|
196
207
|
}
|
|
197
208
|
|
|
209
|
+
// ── Arrow-key history navigation (insert mode only, Claude Code parity) ──
|
|
210
|
+
if (vimEngine.current.mode === 'insert' && !searchMode) {
|
|
211
|
+
if (key.upArrow || key.downArrow) {
|
|
212
|
+
// Ignore if picker is active (command picker handles its own arrows)
|
|
213
|
+
if (value.startsWith('/')) return
|
|
214
|
+
|
|
215
|
+
if (key.upArrow) {
|
|
216
|
+
if (submittedHistory.length === 0) return
|
|
217
|
+
// Save current draft the first time we enter history browsing
|
|
218
|
+
if (historyIndexRef.current === -1) {
|
|
219
|
+
savedDraftRef.current = value
|
|
220
|
+
}
|
|
221
|
+
const newIndex = Math.min(historyIndexRef.current + 1, submittedHistory.length - 1)
|
|
222
|
+
historyIndexRef.current = newIndex
|
|
223
|
+
setValue(submittedHistory[submittedHistory.length - 1 - newIndex]!)
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
if (key.downArrow) {
|
|
227
|
+
if (historyIndexRef.current === -1) return
|
|
228
|
+
const newIndex = historyIndexRef.current - 1
|
|
229
|
+
historyIndexRef.current = newIndex
|
|
230
|
+
if (newIndex === -1) {
|
|
231
|
+
// Back to the original draft
|
|
232
|
+
setValue(savedDraftRef.current)
|
|
233
|
+
savedDraftRef.current = ''
|
|
234
|
+
} else {
|
|
235
|
+
setValue(submittedHistory[submittedHistory.length - 1 - newIndex]!)
|
|
236
|
+
}
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
198
242
|
if (vimEngine.current.mode !== 'normal') return
|
|
199
243
|
|
|
200
244
|
// Handle search mode — collect query characters
|
|
@@ -288,6 +332,10 @@ export function InputBar({
|
|
|
288
332
|
|
|
289
333
|
const handleSubmit = (val: string) => {
|
|
290
334
|
if (!val.trim() || isLoading) return
|
|
335
|
+
// Save to message history for arrow-key navigation
|
|
336
|
+
setSubmittedHistory((prev) => [...prev, val])
|
|
337
|
+
historyIndexRef.current = -1
|
|
338
|
+
savedDraftRef.current = ''
|
|
291
339
|
onSubmit(val)
|
|
292
340
|
setValue('')
|
|
293
341
|
setPickerActive(false)
|
|
@@ -327,6 +375,11 @@ export function InputBar({
|
|
|
327
375
|
onChange={(val) => {
|
|
328
376
|
// Block text changes during search mode — keys go to search query
|
|
329
377
|
if (searchMode) return
|
|
378
|
+
// Reset history browsing when user starts typing
|
|
379
|
+
if (historyIndexRef.current !== -1) {
|
|
380
|
+
historyIndexRef.current = -1
|
|
381
|
+
savedDraftRef.current = ''
|
|
382
|
+
}
|
|
330
383
|
setValue(val)
|
|
331
384
|
}}
|
|
332
385
|
onSubmit={handleSubmit}
|