@miphamai/cli 0.26.0 → 0.27.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -9,6 +9,53 @@ import type { PermissionRuleEntry } from '../shared/index.ts'
9
9
  import { matchBashRule, compileRule } from './permission-rules'
10
10
  import { loadPermissionConfig, nextMode, clampMode, MODE_CYCLE } from './permission-config'
11
11
 
12
+ /**
13
+ * Check if a Bash command is a "verification-only" command that should be
14
+ * auto-approved in acceptEdits mode. These are non-destructive read/check
15
+ * operations that form the core of the vibe coding edit→test→fix loop.
16
+ */
17
+ function isVerificationCommand(input: Record<string, unknown>): boolean {
18
+ const cmd = (input.command as string) || ''
19
+ // Patterns for verification-only commands (no side effects on codebase)
20
+ const verifyPatterns = [
21
+ /\bpnpm\s+test\b/, // test runner
22
+ /\bpnpm\s+t\b/, // shorthand test
23
+ /\bpnpm\s+typecheck\b/, // type checking
24
+ /\bpnpm\s+lint\b/, // linting
25
+ /\bpnpm\s+format:check\b/, // format check
26
+ /\bnpm\s+test\b/, // npm test
27
+ /\bnpm\s+run\s+test\b/, // npm run test
28
+ /\bvitest\b/, // vitest runner
29
+ /\bjest\b/, // jest runner
30
+ /\btsc\s+(?!init)/, // TypeScript compiler (not tsc init)
31
+ /\btsc\s+--noEmit\b/, // type check only
32
+ /\beslint\b/, // eslint
33
+ /\bprettier\s+--check\b/, // prettier check
34
+ /\bpytest\b/, // python test runner
35
+ /\bruff\s+check\b/, // python linter
36
+ /\bcargo\s+test\b/, // rust test
37
+ /\bcargo\s+check\b/, // rust check
38
+ /\bgo\s+test\b/, // go test
39
+ /\bgo\s+vet\b/, // go vet
40
+ /\bmake\s+test\b/, // make test
41
+ /\bgit\s+status\b/, // git status (read-only)
42
+ /\bgit\s+diff\b/, // git diff (read-only)
43
+ /\bgit\s+log\b/, // git log (read-only)
44
+ /\bgit\s+branch\b/, // git branch (read-only)
45
+ /\bls\b/, // list files
46
+ /\bcat\b/, // read file
47
+ /\bhead\b/, // read file start
48
+ /\btail\b/, // read file end
49
+ /\bwhich\b/, // find binary
50
+ /\becho\b/, // print text
51
+ /\bnode\s+-v\b/, // node version
52
+ /\bpython\s+--version\b/, // python version
53
+ /\bwhoami\b/, // current user
54
+ /\bpwd\b/, // current directory
55
+ ]
56
+ return verifyPatterns.some((p) => p.test(cmd))
57
+ }
58
+
12
59
  const VALID_MODES: Set<string> = new Set<string>(MODE_CYCLE)
13
60
 
14
61
  export class PermissionSystem {
@@ -276,7 +323,7 @@ export class PermissionSystem {
276
323
  }
277
324
 
278
325
  // 5. Mode baseline
279
- const baseline = this.modeBaseline(tool)
326
+ const baseline = this.modeBaseline(tool, input)
280
327
  if (baseline !== 'mode-baseline') {
281
328
  this.checkCache.set(cacheKey, baseline)
282
329
  return baseline
@@ -323,21 +370,29 @@ export class PermissionSystem {
323
370
  return rule.pattern === tool.name || rule.compiled.test(tool.name)
324
371
  }
325
372
 
326
- private modeBaseline(tool: ToolDefinition): PermissionLevel | 'mode-baseline' {
373
+ private modeBaseline(
374
+ tool: ToolDefinition,
375
+ input?: Record<string, unknown>,
376
+ ): PermissionLevel | 'mode-baseline' {
327
377
  switch (this.mode) {
328
378
  case 'default':
329
379
  // Delegate to tool.permission (backward compat)
330
380
  return 'mode-baseline'
331
381
 
332
382
  case 'acceptEdits':
333
- // Reads + file edits free; Bash requires approval
334
- return tool.category === 'file'
335
- ? ['Bash'].includes(tool.name)
336
- ? 'ask'
337
- : 'bypass'
338
- : tool.name === 'Bash'
339
- ? 'ask'
340
- : 'ask'
383
+ // Reads + file edits free; Bash auto-approved for verification commands
384
+ if (tool.category === 'file' && tool.name !== 'Bash') {
385
+ return 'bypass'
386
+ }
387
+ if (tool.name === 'Bash') {
388
+ // Vibe coding fix: auto-approve verification commands
389
+ // so the edit→test→fix loop isn't interrupted by permission prompts
390
+ if (input && isVerificationCommand(input)) {
391
+ return 'bypass'
392
+ }
393
+ return 'ask'
394
+ }
395
+ return 'ask'
341
396
 
342
397
  case 'plan':
343
398
  // Only reads, no writes or executes
@@ -80,12 +80,12 @@ export const enterPlanModeTool: ToolDefinition = {
80
80
  ' ⚠️ All other tools — require confirmation',
81
81
  '',
82
82
  'Design your approach, explore the codebase, then:',
83
- ' • Use ExitPlanMode to submit your plan for approval',
84
- ' • The user will review and approve before code changes begin',
83
+ ' • Use ExitPlanMode to submit your plan for user review',
84
+ ' • Present your plan and ask for explicit approval',
85
+ ' • The user must say "approved" before you switch to acceptEdits mode',
85
86
  '',
86
- 'ExitPlanMode parameters:',
87
- ' approved: true → switch to acceptEdits mode (code changes allowed)',
88
- ' approved: false → revert to default mode',
87
+ '⚠️ You CANNOT self-approve your plan. The user must explicitly confirm.',
88
+ ' After ExitPlanMode, wait for user approval before making code changes.',
89
89
  ].join('\n'),
90
90
  }
91
91
  },
@@ -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 submit your plan for approval. ' +
7
- 'Set approved: true to switch to acceptEdits mode (code changes allowed). ' +
8
- 'Set approved: false to revert to default mode. ' +
9
- 'Use this after you have finished designing your approach in plan mode.',
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
- approved: {
16
- type: 'boolean',
17
+ planFile: {
18
+ type: 'string',
17
19
  description:
18
- 'Whether the user approved the plan. true → switch to acceptEdits mode. false → revert to default.',
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: ['approved'],
23
+ required: [],
22
24
  },
23
- async execute(params, _ctx) {
24
- const approved = params.approved === true
25
+ async execute(params, ctx) {
26
+ const planDir = join(ctx.cwd, '.mipham', 'plans')
27
+ const planFile = (params.planFile as string) || ''
25
28
 
26
- if (approved) {
27
- return {
28
- success: true,
29
- content: [
30
- '── Plan Approved ──',
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 Mode Exited ──',
43
+ '── Plan Ready for Review ──',
46
44
  '',
47
- '✓ Returning to default permission mode.',
45
+ '✓ Exiting plan mode.',
46
+ '✓ Plan file saved. Present your plan to the user now.',
48
47
  '',
49
- 'No code changes were made. The plan file is preserved in .mipham/plans/.',
50
- 'Use EnterPlanMode again when ready to resume planning.',
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
  },
@@ -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,