@miphamai/cli 0.71.0 → 0.73.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/src/commands/project.ts +1 -3
- package/src/core/permission-rules.ts +220 -3
- package/src/core/permission.ts +18 -0
- package/src/daemon/server.ts +3 -0
- package/src/index.tsx +6 -0
- 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/network/web-fetch.ts +1 -1
- package/src/tools/network/web-search.ts +3 -1
- 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
|
}
|
|
@@ -1,6 +1,182 @@
|
|
|
1
1
|
import type { PermissionRuleEntry } from '../shared/index.ts'
|
|
2
2
|
import { matchPath } from './credential-masker/matcher'
|
|
3
3
|
|
|
4
|
+
// ── Bash command analysis (Read/Write/Edit deny-rule extension) ──
|
|
5
|
+
//
|
|
6
|
+
// A Read(/etc/passwd) deny rule must also refuse Bash commands that read that
|
|
7
|
+
// file (`cat /etc/passwd`, `tac /etc/passwd`, `grep x /etc/passwd`,
|
|
8
|
+
// `< /etc/passwd`) — not only the Read tool. Same for Write/Edit rules and
|
|
9
|
+
// `> file` redirects / in-place editors. These helpers map a Bash command to
|
|
10
|
+
// the file paths it touches, conservatively: over-matching is the safe
|
|
11
|
+
// direction for a deny rule, under-matching is not.
|
|
12
|
+
|
|
13
|
+
/** Commands that read a file and print its contents (bypass Read() rules today). */
|
|
14
|
+
const READER_COMMANDS = new Set([
|
|
15
|
+
'cat',
|
|
16
|
+
'tac',
|
|
17
|
+
'head',
|
|
18
|
+
'tail',
|
|
19
|
+
'less',
|
|
20
|
+
'more',
|
|
21
|
+
'grep',
|
|
22
|
+
'egrep',
|
|
23
|
+
'fgrep',
|
|
24
|
+
'rg',
|
|
25
|
+
'sed',
|
|
26
|
+
'perl',
|
|
27
|
+
'awk',
|
|
28
|
+
'nl',
|
|
29
|
+
'sort',
|
|
30
|
+
'uniq',
|
|
31
|
+
'cut',
|
|
32
|
+
'diff',
|
|
33
|
+
'comm',
|
|
34
|
+
'join',
|
|
35
|
+
'paste',
|
|
36
|
+
'file',
|
|
37
|
+
'stat',
|
|
38
|
+
'xxd',
|
|
39
|
+
'od',
|
|
40
|
+
'hexdump',
|
|
41
|
+
'strings',
|
|
42
|
+
'wc',
|
|
43
|
+
'view',
|
|
44
|
+
'zcat',
|
|
45
|
+
'bzcat',
|
|
46
|
+
])
|
|
47
|
+
|
|
48
|
+
/** Commands that write/modify a file (bypass Write()/Edit() rules today). */
|
|
49
|
+
const WRITER_COMMANDS = new Set([
|
|
50
|
+
'tee',
|
|
51
|
+
'touch',
|
|
52
|
+
'nano',
|
|
53
|
+
'vi',
|
|
54
|
+
'vim',
|
|
55
|
+
'emacs',
|
|
56
|
+
'ed',
|
|
57
|
+
'ex',
|
|
58
|
+
'cp',
|
|
59
|
+
'mv',
|
|
60
|
+
'rm',
|
|
61
|
+
'dd',
|
|
62
|
+
'install',
|
|
63
|
+
'truncate',
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Split a (possibly compound) shell command into simple-command segments, so a
|
|
68
|
+
* Bash(pattern) rule matches any segment rather than only the whole string
|
|
69
|
+
* (`rm -rf /` buried in `foo && rm -rf /` must still match).
|
|
70
|
+
*
|
|
71
|
+
* Separators: `&&` `||` `;` `|` `&` and newline. Redirect operators (`>`,
|
|
72
|
+
* `>>`, `<`) are NOT separators — they belong to the same simple command.
|
|
73
|
+
* Quoting is not parsed (a `&` inside quotes still splits), which only
|
|
74
|
+
* over-matches — the safe direction for deny rules.
|
|
75
|
+
*/
|
|
76
|
+
export function splitShellSegments(command: string): string[] {
|
|
77
|
+
return command
|
|
78
|
+
.split(/&&|\|\||;|\||&|\n|\r/g)
|
|
79
|
+
.map((s) => s.trim())
|
|
80
|
+
.filter((s) => s.length > 0)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function stripQuotes(s: string): string {
|
|
84
|
+
if (s.length >= 2) {
|
|
85
|
+
const first = s[0]
|
|
86
|
+
const last = s[s.length - 1]
|
|
87
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
88
|
+
return s.slice(1, -1)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return s
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function uniq(items: string[]): string[] {
|
|
95
|
+
return [...new Set(items)]
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** File paths a Bash command reads or writes. */
|
|
99
|
+
export interface BashFileAccess {
|
|
100
|
+
read: string[]
|
|
101
|
+
write: string[]
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract the file paths a Bash command reads or writes, so Read()/Write()/
|
|
106
|
+
* Edit() deny rules also apply to Bash (not just the Read/Write/Edit tools).
|
|
107
|
+
* Covers redirects (`<` reads, `>`/`>>` writes) and reader/writer command
|
|
108
|
+
* arguments — including inside `$(...)` and backtick substitutions. Deliberately
|
|
109
|
+
* conservative, not a full shell parser.
|
|
110
|
+
*/
|
|
111
|
+
export function extractBashFileAccess(command: string): BashFileAccess {
|
|
112
|
+
const read: string[] = []
|
|
113
|
+
const write: string[] = []
|
|
114
|
+
|
|
115
|
+
// 1. Redirect targets. Handles `> file` and `>file`, optional fd prefix
|
|
116
|
+
// (`2>`, `&>`). Heredocs (`<<`) and fd-dup targets (`2>&1`) are skipped —
|
|
117
|
+
// their "target" is a delimiter or file descriptor, not a path.
|
|
118
|
+
const redirectRe = /(?:^|[\s;|&])([0-9]*&?)?(>>|<<|<|>)\s*([^\s;|&<>]+)/g
|
|
119
|
+
let m: RegExpExecArray | null
|
|
120
|
+
while ((m = redirectRe.exec(command)) !== null) {
|
|
121
|
+
const op = m[2]!
|
|
122
|
+
if (op === '<<') continue // heredoc delimiter
|
|
123
|
+
const target = stripQuotes(m[3]!)
|
|
124
|
+
if (op === '<') read.push(target)
|
|
125
|
+
else write.push(target)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 2. Reader/writer command arguments (non-flag args are candidate paths),
|
|
129
|
+
// recursing into `$(...)` / backtick substitutions.
|
|
130
|
+
scanReaderWriterCommands(command, read, write)
|
|
131
|
+
|
|
132
|
+
return { read: uniq(read), write: uniq(write) }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Extract the inner commands of `$(...)` and backtick substitutions. */
|
|
136
|
+
function extractSubstitutions(command: string): string[] {
|
|
137
|
+
const inners: string[] = []
|
|
138
|
+
let m: RegExpExecArray | null
|
|
139
|
+
// `$(...)` — non-nested groups; deeper nesting is handled by the recursion
|
|
140
|
+
// in scanReaderWriterCommands (each level is extracted on the next pass).
|
|
141
|
+
const dollarParen = /\$\(([^()]*)\)/g
|
|
142
|
+
while ((m = dollarParen.exec(command)) !== null) inners.push(m[1]!)
|
|
143
|
+
const backtick = /`([^`]*)`/g
|
|
144
|
+
while ((m = backtick.exec(command)) !== null) inners.push(m[1]!)
|
|
145
|
+
return inners
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Detect reader/writer commands at the front of each shell segment and recurse
|
|
150
|
+
* into command substitutions, so `echo $(cat .git-credentials)` is caught.
|
|
151
|
+
*/
|
|
152
|
+
function scanReaderWriterCommands(command: string, read: string[], write: string[]): void {
|
|
153
|
+
for (const seg of splitShellSegments(command)) {
|
|
154
|
+
const tokens = seg.split(/\s+/).filter(Boolean)
|
|
155
|
+
if (tokens.length > 0) {
|
|
156
|
+
const base = (tokens[0] || '').split('/').pop() || ''
|
|
157
|
+
const args = tokens.slice(1)
|
|
158
|
+
if (READER_COMMANDS.has(base)) {
|
|
159
|
+
// `sed -i` / `perl -i` read AND write their file args.
|
|
160
|
+
const inPlace = args.some((a) => a === '-i' || a.startsWith('--in-place'))
|
|
161
|
+
for (const arg of args) {
|
|
162
|
+
if (arg.startsWith('-')) continue
|
|
163
|
+
const p = stripQuotes(arg)
|
|
164
|
+
read.push(p)
|
|
165
|
+
if (inPlace) write.push(p)
|
|
166
|
+
}
|
|
167
|
+
} else if (WRITER_COMMANDS.has(base)) {
|
|
168
|
+
for (const arg of args) {
|
|
169
|
+
if (arg.startsWith('-')) continue
|
|
170
|
+
write.push(stripQuotes(arg))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
for (const inner of extractSubstitutions(seg)) {
|
|
175
|
+
scanReaderWriterCommands(inner, read, write)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
4
180
|
// Match a tool(parameter) rule against an actual tool call.
|
|
5
181
|
//
|
|
6
182
|
// Pattern formats:
|
|
@@ -25,12 +201,24 @@ export function matchBashRule(
|
|
|
25
201
|
|
|
26
202
|
const [, baseTool, subPattern] = parenMatch
|
|
27
203
|
|
|
204
|
+
// A Read/Write/Edit rule must also refuse a Bash command that touches the
|
|
205
|
+
// same file (via a reader/editor command or a redirect), not only the
|
|
206
|
+
// Read/Write/Edit tool itself. Otherwise `cat .git-credentials` bypasses a
|
|
207
|
+
// `Read(.git-credentials)` deny rule.
|
|
208
|
+
if (toolName === 'Bash' && (baseTool === 'Read' || baseTool === 'Write' || baseTool === 'Edit')) {
|
|
209
|
+
const cmd = String(toolInput.command || '')
|
|
210
|
+
const access = extractBashFileAccess(cmd)
|
|
211
|
+
const paths = baseTool === 'Read' ? access.read : access.write
|
|
212
|
+
return paths.some((p) => matchPath(p, subPattern!))
|
|
213
|
+
}
|
|
214
|
+
|
|
28
215
|
if (toolName !== baseTool!) return false
|
|
29
216
|
|
|
30
|
-
// For Bash: match against the command string
|
|
217
|
+
// For Bash: match against the command string (any segment of a compound
|
|
218
|
+
// command — `rm -rf /` buried in `foo && rm -rf /` still matches).
|
|
31
219
|
if (baseTool === 'Bash') {
|
|
32
220
|
const cmd = String(toolInput.command || '')
|
|
33
|
-
return wildcardMatch(subPattern!,
|
|
221
|
+
return splitShellSegments(cmd).some((seg) => wildcardMatch(subPattern!, seg))
|
|
34
222
|
}
|
|
35
223
|
|
|
36
224
|
// For Write/Edit/Read: match against the file_path with path-glob semantics.
|
|
@@ -63,8 +251,37 @@ export function wildcardMatch(pattern: string, input: string): boolean {
|
|
|
63
251
|
return new RegExp(regexStr).test(input)
|
|
64
252
|
}
|
|
65
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
|
+
|
|
66
282
|
/** Compile a rule pattern string into a PermissionRuleEntry. */
|
|
67
283
|
export function compileRule(pattern: string, level: 'allow' | 'deny' | 'ask'): PermissionRuleEntry {
|
|
284
|
+
const invalid = validateRulePattern(pattern)
|
|
68
285
|
const regexStr =
|
|
69
286
|
'^' +
|
|
70
287
|
pattern
|
|
@@ -73,5 +290,5 @@ export function compileRule(pattern: string, level: 'allow' | 'deny' | 'ask'): P
|
|
|
73
290
|
.replace(/\\\*/g, '.*')
|
|
74
291
|
.replace(/\\\?/g, '.') +
|
|
75
292
|
'$'
|
|
76
|
-
return { pattern, level, compiled: new RegExp(regexStr) }
|
|
293
|
+
return { pattern, level, compiled: new RegExp(regexStr), ...(invalid ? { invalid } : {}) }
|
|
77
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
|
|
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()
|
|
@@ -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.0' 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 {
|
|
@@ -276,7 +276,7 @@ export const webFetchTool: ToolDefinition = {
|
|
|
276
276
|
const message =
|
|
277
277
|
err instanceof Error && err.name === 'AbortError'
|
|
278
278
|
? 'Request timed out (30s)'
|
|
279
|
-
: `Fetch failed: ${String(err)}
|
|
279
|
+
: `Fetch failed: ${String(err)}. If direct network is blocked, retry via the web-access skill (CDP through the user's logged-in Chrome).`
|
|
280
280
|
return { success: false, content: '', error: message }
|
|
281
281
|
}
|
|
282
282
|
},
|
|
@@ -126,6 +126,8 @@ export const webSearchTool: ToolDefinition = {
|
|
|
126
126
|
'Alternatives (add API key for any):',
|
|
127
127
|
' TAVILY_API_KEY — https://tavily.com',
|
|
128
128
|
' SERPAPI_API_KEY — https://serpapi.com',
|
|
129
|
+
'',
|
|
130
|
+
'No API key? Use the web-access skill (CDP through your logged-in Chrome) for live web search.',
|
|
129
131
|
].join('\n'),
|
|
130
132
|
}
|
|
131
133
|
}
|
|
@@ -199,7 +201,7 @@ export const webSearchTool: ToolDefinition = {
|
|
|
199
201
|
const message =
|
|
200
202
|
err instanceof Error && err.name === 'AbortError'
|
|
201
203
|
? 'Search timed out (15s). Try a more specific query.'
|
|
202
|
-
: `Search failed: ${String(err)}
|
|
204
|
+
: `Search failed: ${String(err)}. If direct network is blocked, retry via the web-access skill (CDP through the user's logged-in Chrome).`
|
|
203
205
|
return { success: false, content: '', error: message }
|
|
204
206
|
}
|
|
205
207
|
},
|
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
|
|