@miphamai/cli 0.72.0 → 0.73.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/project.ts +1 -3
- package/src/core/permission-rules.ts +30 -1
- package/src/core/permission.ts +18 -0
- package/src/daemon/server.ts +3 -0
- package/src/i18n-core/locales/en-US.json +1 -1
- package/src/i18n-core/locales/zh-CN.json +1 -1
- package/src/index.tsx +37 -26
- package/src/shared/format.ts +15 -0
- package/src/shared/mipham-models.json +13 -13
- package/src/shared/package-info.ts +1 -1
- package/src/shared/types.ts +2 -0
- package/src/tools/file/grep.ts +48 -2
- package/src/ui/commands.ts +2 -1
- package/src/ui/picker.tsx +1 -1
package/package.json
CHANGED
package/src/commands/project.ts
CHANGED
|
@@ -553,9 +553,7 @@ async function setupStep3(ctx: CommandContext): Promise<CommandResult> {
|
|
|
553
553
|
lines.push(` ${p.id}${p.id === ctx.providerId ? ' ← current' : ''}`)
|
|
554
554
|
for (const m of p.models.filter((m) => m.status === 'active')) {
|
|
555
555
|
const marker = m.id === ctx.modelId ? ' ★' : ' '
|
|
556
|
-
lines.push(
|
|
557
|
-
`${marker} ${m.id.padEnd(30)} ${m.contextWindow.toLocaleString()} ctx ${m.vision ? '🖼' : '📝'}`,
|
|
558
|
-
)
|
|
556
|
+
lines.push(`${marker} ${m.id.padEnd(30)} ${m.vision ? '🖼' : '📝'}`)
|
|
559
557
|
}
|
|
560
558
|
lines.push('')
|
|
561
559
|
}
|
|
@@ -251,8 +251,37 @@ export function wildcardMatch(pattern: string, input: string): boolean {
|
|
|
251
251
|
return new RegExp(regexStr).test(input)
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
/**
|
|
255
|
+
* Validate a rule pattern string's structure, mirroring exactly what
|
|
256
|
+
* `matchBashRule` / `ruleMatches` will actually match. A pattern that is
|
|
257
|
+
* syntactically valid but never matches (e.g. `Bash(ls) x`, `Read(foo`,
|
|
258
|
+
* `Bash()`) is silently dead today — this returns a human-readable reason so
|
|
259
|
+
* the caller can report it as an invalid setting instead of ignoring it.
|
|
260
|
+
*
|
|
261
|
+
* Returns null when the pattern is valid, or a reason string when malformed.
|
|
262
|
+
*/
|
|
263
|
+
export function validateRulePattern(pattern: string): string | null {
|
|
264
|
+
if (!pattern.trim()) return 'rule pattern is empty'
|
|
265
|
+
|
|
266
|
+
if (pattern.includes('(')) {
|
|
267
|
+
if (!pattern.includes(')')) return 'unclosed parenthesis'
|
|
268
|
+
// Empty parameter: `Bash()` or `Bash( )`
|
|
269
|
+
if (/\(\s*\)$/.test(pattern)) return 'empty parameter'
|
|
270
|
+
// Must be exactly `ToolName(param)` with nothing before or after.
|
|
271
|
+
if (!/^(\w+)\((.+)\)$/.test(pattern)) {
|
|
272
|
+
return 'unexpected text after the closing parenthesis'
|
|
273
|
+
}
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// No parenthesis → must be a plain tool name (matched via `pattern === tool.name`).
|
|
278
|
+
if (!/^\w+$/.test(pattern)) return 'not a single tool name'
|
|
279
|
+
return null
|
|
280
|
+
}
|
|
281
|
+
|
|
254
282
|
/** Compile a rule pattern string into a PermissionRuleEntry. */
|
|
255
283
|
export function compileRule(pattern: string, level: 'allow' | 'deny' | 'ask'): PermissionRuleEntry {
|
|
284
|
+
const invalid = validateRulePattern(pattern)
|
|
256
285
|
const regexStr =
|
|
257
286
|
'^' +
|
|
258
287
|
pattern
|
|
@@ -261,5 +290,5 @@ export function compileRule(pattern: string, level: 'allow' | 'deny' | 'ask'): P
|
|
|
261
290
|
.replace(/\\\*/g, '.*')
|
|
262
291
|
.replace(/\\\?/g, '.') +
|
|
263
292
|
'$'
|
|
264
|
-
return { pattern, level, compiled: new RegExp(regexStr) }
|
|
293
|
+
return { pattern, level, compiled: new RegExp(regexStr), ...(invalid ? { invalid } : {}) }
|
|
265
294
|
}
|
package/src/core/permission.ts
CHANGED
|
@@ -489,6 +489,24 @@ export class PermissionSystem {
|
|
|
489
489
|
this.invalidateCache()
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
+
/**
|
|
493
|
+
* Report any rule whose pattern is structurally invalid and can therefore
|
|
494
|
+
* never match (e.g. `Bash(ls) x`, `Read(foo`, `Bash()`). These are silently
|
|
495
|
+
* dead today — callers should surface them as invalid settings rather than
|
|
496
|
+
* let a deny rule fail closed without the user noticing.
|
|
497
|
+
*/
|
|
498
|
+
getInvalidRules(): string[] {
|
|
499
|
+
const invalid: string[] = []
|
|
500
|
+
for (const entry of [...this.allowRules, ...this.denyRules, ...this.askRules]) {
|
|
501
|
+
if (entry.invalid) {
|
|
502
|
+
invalid.push(
|
|
503
|
+
`permission rule "${entry.pattern}" is invalid (${entry.invalid}) and will never match`,
|
|
504
|
+
)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return invalid
|
|
508
|
+
}
|
|
509
|
+
|
|
492
510
|
removeRule(toolName: string): void {
|
|
493
511
|
this.legacyRules.delete(toolName)
|
|
494
512
|
this.removeRuleFromArrays(toolName)
|
package/src/daemon/server.ts
CHANGED
|
@@ -95,6 +95,9 @@ export function buildDaemonPermission(
|
|
|
95
95
|
for (const rule of rules.allow ?? []) permission.allow(rule)
|
|
96
96
|
for (const rule of rules.deny ?? []) permission.deny(rule)
|
|
97
97
|
}
|
|
98
|
+
for (const msg of permission.getInvalidRules()) {
|
|
99
|
+
process.stderr.write(`⚠ Mipham Code: ${msg}\n`)
|
|
100
|
+
}
|
|
98
101
|
return permission
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -780,7 +780,7 @@
|
|
|
780
780
|
"ui": {
|
|
781
781
|
"banner": {
|
|
782
782
|
"title": "Mipham Code",
|
|
783
|
-
"subtitle": "AI-Powered
|
|
783
|
+
"subtitle": "AI-Powered Super Agent",
|
|
784
784
|
"tagline": "Multi-model / Multi-provider / Skills & Tools / Open-core",
|
|
785
785
|
"start_message": "Type a message to start. /help for commands",
|
|
786
786
|
"controls_hint": "Ctrl+P pick model · Esc to exit",
|
package/src/index.tsx
CHANGED
|
@@ -559,6 +559,12 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
559
559
|
for (const rule of settingsJson.permissions.allow) engine.getPermission().allow(rule)
|
|
560
560
|
for (const rule of settingsJson.permissions.deny) engine.getPermission().deny(rule)
|
|
561
561
|
|
|
562
|
+
// Surface malformed permission rules as warnings instead of silently letting
|
|
563
|
+
// them never match (e.g. a deny rule typed `Bash(ls) x` would fail closed).
|
|
564
|
+
for (const msg of engine.getPermission().getInvalidRules()) {
|
|
565
|
+
process.stderr.write(`⚠ Mipham Code: ${msg}\n`)
|
|
566
|
+
}
|
|
567
|
+
|
|
562
568
|
// Initialize agent registry and load plugin agents/skills/MCP/hooks
|
|
563
569
|
const agentRegistry = new AgentRegistry()
|
|
564
570
|
agentRegistry.loadUserAgents()
|
|
@@ -624,6 +630,28 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
624
630
|
|
|
625
631
|
// Auto-save session on exit
|
|
626
632
|
let saved = false
|
|
633
|
+
|
|
634
|
+
// Persist the current session under its real name. Shared by saveAndExit and
|
|
635
|
+
// the process 'exit' safety net — the two paths must stay identical, or the
|
|
636
|
+
// session gets saved under a different (timestamped) name and /resume breaks.
|
|
637
|
+
const persistSession = () => {
|
|
638
|
+
if (context.getMessageCount() === 0) return
|
|
639
|
+
const log = context.getLog()
|
|
640
|
+
if (log) {
|
|
641
|
+
SessionStore.saveLog(sessionName, log, {
|
|
642
|
+
provider: defaultProvider,
|
|
643
|
+
model: defaultModel,
|
|
644
|
+
cwd: process.cwd(),
|
|
645
|
+
})
|
|
646
|
+
} else {
|
|
647
|
+
SessionStore.save(sessionName, context.getMessages(), {
|
|
648
|
+
provider: defaultProvider,
|
|
649
|
+
model: defaultModel,
|
|
650
|
+
cwd: process.cwd(),
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
627
655
|
const saveAndExit = () => {
|
|
628
656
|
saved = true
|
|
629
657
|
clearInterval(heartbeatInterval)
|
|
@@ -631,22 +659,7 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
631
659
|
// P2-1: Trigger SessionEnd hooks before cleanup (best-effort)
|
|
632
660
|
hookEngine.executeSessionEnd(sessionName).catch(() => {})
|
|
633
661
|
artifactServer.stop()
|
|
634
|
-
|
|
635
|
-
const log = context.getLog()
|
|
636
|
-
if (log) {
|
|
637
|
-
SessionStore.saveLog(sessionName, log, {
|
|
638
|
-
provider: defaultProvider,
|
|
639
|
-
model: defaultModel,
|
|
640
|
-
cwd: process.cwd(),
|
|
641
|
-
})
|
|
642
|
-
} else {
|
|
643
|
-
SessionStore.save(sessionName, context.getMessages(), {
|
|
644
|
-
provider: defaultProvider,
|
|
645
|
-
model: defaultModel,
|
|
646
|
-
cwd: process.cwd(),
|
|
647
|
-
})
|
|
648
|
-
}
|
|
649
|
-
}
|
|
662
|
+
persistSession()
|
|
650
663
|
// Finalize the session — write session summary + flush CRSI effectiveness
|
|
651
664
|
// (evaluate rules and apply auto-degrade/disable). Best-effort: the
|
|
652
665
|
// self-improvement closeout must never block session exit.
|
|
@@ -661,27 +674,25 @@ export async function runApp(options: RunOptions): Promise<void> {
|
|
|
661
674
|
|
|
662
675
|
process.on('SIGINT', saveAndExit)
|
|
663
676
|
process.on('SIGTERM', saveAndExit)
|
|
677
|
+
// Terminal close / SSH disconnect sends SIGHUP (not SIGINT/SIGTERM); without
|
|
678
|
+
// a handler the session log is silently dropped on those paths.
|
|
679
|
+
process.on('SIGHUP', saveAndExit)
|
|
664
680
|
|
|
665
|
-
// Safety net:
|
|
681
|
+
// Safety net: persist on exit for paths that bypass saveAndExit (e.g. /exit
|
|
682
|
+
// and /quit call process.exit(0) directly). saveAndExit already persists;
|
|
683
|
+
// guard on !saved to avoid double-writing.
|
|
666
684
|
process.on('exit', () => {
|
|
667
685
|
clearInterval(heartbeatInterval)
|
|
668
686
|
unregisterSession(sessionName)
|
|
669
687
|
// Finalize the session on non-interactive exit paths
|
|
670
|
-
// (daemon worker / crash / kill) that bypass saveAndExit.
|
|
671
|
-
// already finalizes; guard on !saved to avoid double-evaluating.
|
|
688
|
+
// (daemon worker / crash / kill) that bypass saveAndExit.
|
|
672
689
|
if (!saved) {
|
|
673
690
|
try {
|
|
674
691
|
engine.getAutoMemory().finalizeSession()
|
|
675
692
|
} catch {
|
|
676
693
|
// ignore — CRSI closeout is non-critical
|
|
677
694
|
}
|
|
678
|
-
|
|
679
|
-
if (!saved && context.getMessageCount() > 0) {
|
|
680
|
-
SessionStore.autoSave(context.getMessages(), {
|
|
681
|
-
provider: defaultProvider,
|
|
682
|
-
model: defaultModel,
|
|
683
|
-
cwd: process.cwd(),
|
|
684
|
-
})
|
|
695
|
+
persistSession()
|
|
685
696
|
// Distill learnings from the last 5 user messages into memory
|
|
686
697
|
const allMessages = context.getMessages()
|
|
687
698
|
const userMessages = allMessages
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format a model's context window (token count) into a friendly unit string.
|
|
3
|
+
* Decimal K/M: 16384 → "16K", 131072 → "131K", 200000 → "200K", 1000000 → "1M".
|
|
4
|
+
* Used only on the developer-facing `/models` command — the first-run model
|
|
5
|
+
* picker deliberately omits context window (raw token counts make Mipham's own
|
|
6
|
+
* models look small next to 1M-context competitors).
|
|
7
|
+
*/
|
|
8
|
+
export function formatContextWindow(tokens: number): string {
|
|
9
|
+
if (tokens >= 1_000_000) {
|
|
10
|
+
const m = tokens / 1_000_000
|
|
11
|
+
return `${Number.isInteger(m) ? m : m.toFixed(1)}M`
|
|
12
|
+
}
|
|
13
|
+
if (tokens >= 1_000) return `${Math.round(tokens / 1_000)}K`
|
|
14
|
+
return `${tokens}`
|
|
15
|
+
}
|
|
@@ -11,34 +11,34 @@
|
|
|
11
11
|
"models": [
|
|
12
12
|
{
|
|
13
13
|
"id": "om-v5-flash",
|
|
14
|
-
"name": "
|
|
14
|
+
"name": "om-V5-Flash",
|
|
15
15
|
"providerId": "mipham",
|
|
16
16
|
"contextWindow": 16384,
|
|
17
17
|
"maxOutput": 4096,
|
|
18
18
|
"vision": false,
|
|
19
19
|
"status": "active"
|
|
20
20
|
},
|
|
21
|
-
{
|
|
22
|
-
"id": "om-v5-pro",
|
|
23
|
-
"name": "OM V5 Pro",
|
|
24
|
-
"providerId": "mipham",
|
|
25
|
-
"contextWindow": 131072,
|
|
26
|
-
"maxOutput": 16384,
|
|
27
|
-
"vision": false,
|
|
28
|
-
"status": "active"
|
|
29
|
-
},
|
|
30
21
|
{
|
|
31
22
|
"id": "om-v5-visual",
|
|
32
|
-
"name": "
|
|
23
|
+
"name": "om-V5-Visual",
|
|
33
24
|
"providerId": "mipham",
|
|
34
25
|
"contextWindow": 65536,
|
|
35
26
|
"maxOutput": 4096,
|
|
36
27
|
"vision": true,
|
|
37
28
|
"status": "active"
|
|
38
29
|
},
|
|
30
|
+
{
|
|
31
|
+
"id": "om-v5-pro",
|
|
32
|
+
"name": "om-V5-Pro",
|
|
33
|
+
"providerId": "mipham",
|
|
34
|
+
"contextWindow": 131072,
|
|
35
|
+
"maxOutput": 16384,
|
|
36
|
+
"vision": false,
|
|
37
|
+
"status": "active"
|
|
38
|
+
},
|
|
39
39
|
{
|
|
40
40
|
"id": "om-v5-apex",
|
|
41
|
-
"name": "
|
|
41
|
+
"name": "om-V5-Apex",
|
|
42
42
|
"providerId": "mipham",
|
|
43
43
|
"contextWindow": 200000,
|
|
44
44
|
"maxOutput": 32768,
|
|
@@ -46,5 +46,5 @@
|
|
|
46
46
|
"status": "active"
|
|
47
47
|
}
|
|
48
48
|
],
|
|
49
|
-
"synced_at": "2026-
|
|
49
|
+
"synced_at": "2026-09-04T02:32:33.479Z"
|
|
50
50
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export const PACKAGE_NAME = '@miphamai/cli' as const
|
|
10
10
|
|
|
11
11
|
/** 当前发布版本 */
|
|
12
|
-
export const PACKAGE_VERSION = '0.
|
|
12
|
+
export const PACKAGE_VERSION = '0.73.1' as const
|
|
13
13
|
|
|
14
14
|
/** npm install 全局安装命令 */
|
|
15
15
|
export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
|
package/src/shared/types.ts
CHANGED
|
@@ -315,6 +315,8 @@ export interface PermissionRuleEntry {
|
|
|
315
315
|
pattern: string // e.g., "Bash(git:*)"
|
|
316
316
|
level: 'allow' | 'deny' | 'ask'
|
|
317
317
|
compiled: RegExp
|
|
318
|
+
/** Set when the pattern is structurally invalid and can never match. */
|
|
319
|
+
invalid?: string
|
|
318
320
|
}
|
|
319
321
|
|
|
320
322
|
export interface PermissionRule {
|
package/src/tools/file/grep.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { parse } from 'node:path'
|
|
1
3
|
import type { ToolDefinition, CredentialMaskingConfig } from '../../shared/index.ts'
|
|
2
4
|
import { resolveSafe } from '../../security/path'
|
|
3
5
|
import type { Service } from '../../vajra'
|
|
@@ -37,6 +39,15 @@ export function truncateGrepOutput(stdout: string): string {
|
|
|
37
39
|
return `${out.slice(0, GREP_MAX_OUTPUT_CHARS)}\n\n... (truncated)`
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
/**
|
|
43
|
+
* 判断搜索根是否是「顶层目录」(家目录或文件系统根)。这类范围扫描的是海量
|
|
44
|
+
* 文件树(macOS ~/Library 有数百万受保护文件),会让 rg exit 2、find 回退卡
|
|
45
|
+
* 满 120s。顶层范围应 fail-fast,让模型指定项目目录,而不是静默全盘扫。
|
|
46
|
+
*/
|
|
47
|
+
export function isTopLevelScope(searchPath: string, home = homedir()): boolean {
|
|
48
|
+
return searchPath === home || searchPath === parse(searchPath).root
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
export function createGrepTool(credentialConfig?: CredentialMaskingConfig): ToolDefinition {
|
|
41
52
|
return {
|
|
42
53
|
name: 'Grep',
|
|
@@ -65,6 +76,20 @@ export function createGrepTool(credentialConfig?: CredentialMaskingConfig): Tool
|
|
|
65
76
|
const searchPath = resolveSafe(ctx.cwd, (params.path as string) || '.')
|
|
66
77
|
const include = params.include as string | undefined
|
|
67
78
|
|
|
79
|
+
// Scope guard: a top-level search root (home or filesystem root) scans an
|
|
80
|
+
// enormous tree and stalls the find fallback for minutes. Fail fast and
|
|
81
|
+
// ask for a project-scoped path instead of silently scanning everything.
|
|
82
|
+
if (!params.path && isTopLevelScope(searchPath)) {
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
content: '',
|
|
86
|
+
error:
|
|
87
|
+
`Search scope is the home directory / filesystem root (${searchPath}) — too large ` +
|
|
88
|
+
'and contains protected directories. Specify a project directory with "path", ' +
|
|
89
|
+
'or cd into the project first.',
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
68
93
|
// 1. ripgrep (fast path)
|
|
69
94
|
const rgArgs = ['rg', '-n', '--heading', '--color=never', '-M', '500', pattern]
|
|
70
95
|
if (include) rgArgs.push('--glob', include)
|
|
@@ -85,9 +110,30 @@ export function createGrepTool(credentialConfig?: CredentialMaskingConfig): Tool
|
|
|
85
110
|
content: maskSearchOutput(stdout || '(no matches)', credentialConfig, 'heading'),
|
|
86
111
|
}
|
|
87
112
|
}
|
|
88
|
-
// rg exit 2 (error
|
|
113
|
+
// rg exit 2 (error, e.g. permission denied on protected dirs) — do NOT
|
|
114
|
+
// fall back to the slow `find -type f` scan (it hits the same unreadable
|
|
115
|
+
// paths and stalls on huge trees). Return partial matches if any, else
|
|
116
|
+
// a clear narrow-scope error.
|
|
117
|
+
if (stdout && stdout.trim()) {
|
|
118
|
+
return {
|
|
119
|
+
success: true,
|
|
120
|
+
content: maskSearchOutput(
|
|
121
|
+
stdout +
|
|
122
|
+
'\n\n(rg exited 2 — some paths unreadable; narrow scope for complete results)',
|
|
123
|
+
credentialConfig,
|
|
124
|
+
'heading',
|
|
125
|
+
),
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
content: '',
|
|
131
|
+
error:
|
|
132
|
+
'rg error (exit 2) — likely permission denied on a large/protected tree. ' +
|
|
133
|
+
'Narrow scope with "path" (project directory) and "include".',
|
|
134
|
+
}
|
|
89
135
|
} catch {
|
|
90
|
-
// rg not installed → fall through to grep
|
|
136
|
+
// rg not installed → fall through to grep (find + grep fallback)
|
|
91
137
|
}
|
|
92
138
|
|
|
93
139
|
// 2. fallback: `find -type f -exec grep -Hn {} +`
|
package/src/ui/commands.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { QueryEngine } from '../core/engine'
|
|
8
8
|
import type { MiphamConfig } from '../shared/index.ts'
|
|
9
|
+
import { formatContextWindow } from '../shared/format'
|
|
9
10
|
import type { SkillsLoader } from '../skills/loader'
|
|
10
11
|
import type { PluginManager } from '../plugin/plugin-manager'
|
|
11
12
|
import type { Message } from '../shared/types.js'
|
|
@@ -438,7 +439,7 @@ const modelsCmd: CommandHandler = (ctx) => {
|
|
|
438
439
|
.filter((m) => m.status === 'active')
|
|
439
440
|
.map(
|
|
440
441
|
(m) =>
|
|
441
|
-
` ${p.id.padEnd(12)} ${m.id.padEnd(30)} ${m.contextWindow
|
|
442
|
+
` ${p.id.padEnd(12)} ${m.id.padEnd(30)} ${formatContextWindow(m.contextWindow)} ctx ${m.vision ? '🖼' : '📝'}`,
|
|
442
443
|
),
|
|
443
444
|
)
|
|
444
445
|
|