@miphamai/cli 0.20.0 → 0.20.2
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/agent/sub-agent.ts +4 -2
- package/src/core/engine.ts +19 -12
- package/src/core/hooks-executor.ts +19 -9
- package/src/i18n-context.tsx +2 -6
- package/src/i18n-core/detect.ts +54 -0
- package/src/i18n-core/locales/en-US.json +781 -0
- package/src/i18n-core/locales/zh-CN.json +781 -0
- package/src/i18n-core/t.ts +19 -0
- package/src/i18n-core/types.ts +10 -0
- package/src/index.tsx +9 -6
- package/src/mcp/client.ts +4 -4
- package/src/mcp/oauth.ts +4 -4
- package/src/mcp/registry.ts +9 -5
- package/src/tools/index.ts +9 -5
- package/src/tools/network/web-fetch.ts +15 -0
- package/src/ui/app.tsx +55 -10
- package/src/ui/chat.tsx +18 -12
- package/src/ui/command-picker.tsx +3 -1
- package/src/ui/commands.ts +94 -36
- package/src/ui/picker.tsx +3 -1
package/package.json
CHANGED
package/src/agent/sub-agent.ts
CHANGED
|
@@ -271,8 +271,10 @@ export class SubAgent {
|
|
|
271
271
|
continue
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
// Security: check permission before executing
|
|
275
|
-
// Sub-agents run without user interaction — tools requiring approval are rejected
|
|
274
|
+
// Security: check permission before executing.
|
|
275
|
+
// Sub-agents run without user interaction — tools requiring approval are rejected.
|
|
276
|
+
// When permission system is absent (undefined), allow all tools (backward compat
|
|
277
|
+
// for tests and headless usage). When present, always enforce approval checks.
|
|
276
278
|
if (this.permission?.needsApproval(tool, tu.input)) {
|
|
277
279
|
currentMessages.push({
|
|
278
280
|
role: 'user' as const,
|
package/src/core/engine.ts
CHANGED
|
@@ -18,10 +18,10 @@ import { getBackgroundAgentRegistry } from '../agent/background-registry'
|
|
|
18
18
|
import { RulesLoader } from './rules-loader'
|
|
19
19
|
import { UsageTracker } from './usage-tracker'
|
|
20
20
|
import { buildRequest, sendInferenceCheck, isInferenceHookEnabled } from './inference-hook'
|
|
21
|
-
import { createT } from '
|
|
22
|
-
import enUS from '
|
|
23
|
-
import zhCN from '
|
|
24
|
-
import type { TranslationMap } from '
|
|
21
|
+
import { createT } from '../i18n-core/t'
|
|
22
|
+
import enUS from '../i18n-core/locales/en-US.json'
|
|
23
|
+
import zhCN from '../i18n-core/locales/zh-CN.json'
|
|
24
|
+
import type { TranslationMap } from '../i18n-core/types'
|
|
25
25
|
|
|
26
26
|
const bundles: Record<string, TranslationMap> = {
|
|
27
27
|
'en-US': enUS as TranslationMap,
|
|
@@ -516,9 +516,9 @@ export class QueryEngine {
|
|
|
516
516
|
}
|
|
517
517
|
// Script failed — continue looping
|
|
518
518
|
yield {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
519
|
+
type: 'text',
|
|
520
|
+
content: t('system.goal.failed_continuing', { loop: String(loop) }),
|
|
521
|
+
}
|
|
522
522
|
continue
|
|
523
523
|
}
|
|
524
524
|
|
|
@@ -561,11 +561,14 @@ export class QueryEngine {
|
|
|
561
561
|
private async runScriptVerification(): Promise<boolean> {
|
|
562
562
|
if (!this.goalVerifyScript) return false
|
|
563
563
|
try {
|
|
564
|
-
const {
|
|
565
|
-
|
|
566
|
-
|
|
564
|
+
const { spawnSync } = await import('node:child_process')
|
|
565
|
+
const { resolveSafe } = await import('../security/path')
|
|
566
|
+
// Validate the script path is within the workspace before execution
|
|
567
|
+
const safePath = resolveSafe(process.cwd(), this.goalVerifyScript)
|
|
568
|
+
const result = spawnSync(safePath, [], { timeout: 30000, stdio: 'pipe' })
|
|
569
|
+
return result.status === 0 // exit code 0 = success
|
|
567
570
|
} catch {
|
|
568
|
-
return false // non-zero exit = not yet achieved
|
|
571
|
+
return false // non-zero exit or path rejection = not yet achieved
|
|
569
572
|
}
|
|
570
573
|
}
|
|
571
574
|
|
|
@@ -797,7 +800,11 @@ export class QueryEngine {
|
|
|
797
800
|
|
|
798
801
|
return result
|
|
799
802
|
} catch (err) {
|
|
800
|
-
|
|
803
|
+
// Sanitize error: strip stack traces and internal paths to prevent
|
|
804
|
+
// information disclosure to the LLM conversation context.
|
|
805
|
+
const message =
|
|
806
|
+
err instanceof Error ? err.message : String(err).split('\n')[0] || 'Unknown error'
|
|
807
|
+
return { success: false, content: '', error: `Tool execution failed: ${message}` }
|
|
801
808
|
}
|
|
802
809
|
}
|
|
803
810
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { spawnSync } from 'node:child_process'
|
|
2
2
|
import type { HookConfig, HookContext, HookResult } from '../shared/index.ts'
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -36,21 +36,22 @@ function executeCommand(cfg: HookConfig, ctx: HookContext): HookResult {
|
|
|
36
36
|
try {
|
|
37
37
|
const args = cfg.args ? cfg.args.map((a) => substituteVars(a, ctx)) : []
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
execSync(cmd, {
|
|
39
|
+
// Use spawnSync with array args — no shell, no command injection
|
|
40
|
+
const result = spawnSync(cfg.command, args, {
|
|
42
41
|
timeout: 30_000,
|
|
43
42
|
encoding: 'utf-8',
|
|
44
43
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
45
44
|
})
|
|
46
45
|
|
|
47
46
|
// Exit code 0 = success, allow
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
if (result.status === 0) {
|
|
48
|
+
return { allowed: true }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Non-zero exit: check for block signal (exit code 2)
|
|
52
|
+
const stderr = result.stderr?.toString() || ''
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
if ((err as { status?: number }).status === 2) {
|
|
54
|
+
if (result.status === 2) {
|
|
54
55
|
return {
|
|
55
56
|
allowed: false,
|
|
56
57
|
reason: stderr.trim() || 'Blocked by hook',
|
|
@@ -63,6 +64,15 @@ function executeCommand(cfg: HookConfig, ctx: HookContext): HookResult {
|
|
|
63
64
|
allowed: true,
|
|
64
65
|
additionalContext: `Hook warning (${cfg.command}): ${stderr.trim()}`,
|
|
65
66
|
}
|
|
67
|
+
} catch (err) {
|
|
68
|
+
// spawnSync errors (e.g., command not found, timeout, signal)
|
|
69
|
+
const message = (err as { message?: string }).message || String(err)
|
|
70
|
+
|
|
71
|
+
// Timeout/signal: treat as non-blocking warning
|
|
72
|
+
return {
|
|
73
|
+
allowed: true,
|
|
74
|
+
additionalContext: `Hook error (${cfg.command}): ${message}`,
|
|
75
|
+
}
|
|
66
76
|
}
|
|
67
77
|
}
|
|
68
78
|
|
package/src/i18n-context.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { createContext, useContext } from 'react'
|
|
2
|
-
import type { Locale } from '
|
|
2
|
+
import type { Locale } from './i18n-core/types'
|
|
3
3
|
|
|
4
4
|
export interface I18nContextValue {
|
|
5
5
|
locale: Locale
|
|
@@ -17,11 +17,7 @@ export function I18nProvider({
|
|
|
17
17
|
t: (key: string, params?: Record<string, string>) => string
|
|
18
18
|
children: React.ReactNode
|
|
19
19
|
}) {
|
|
20
|
-
return React.createElement(
|
|
21
|
-
I18nContext.Provider,
|
|
22
|
-
{ value: { locale, t } },
|
|
23
|
-
children,
|
|
24
|
-
)
|
|
20
|
+
return React.createElement(I18nContext.Provider, { value: { locale, t } }, children)
|
|
25
21
|
}
|
|
26
22
|
|
|
27
23
|
export function useI18n(): I18nContextValue {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Locale } from './types'
|
|
2
|
+
import { execSync } from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
export function detectLocale(opts?: { lang?: string; cwd?: string }): Locale {
|
|
5
|
+
// 1. CLI --lang flag
|
|
6
|
+
if (opts?.lang) {
|
|
7
|
+
const normalized = normalizeLocale(opts.lang)
|
|
8
|
+
if (normalized) return normalized as Locale
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 2. LANG / LC_ALL env vars
|
|
12
|
+
const langEnv = process.env.LC_ALL || process.env.LANG || ''
|
|
13
|
+
const envLocale = parseLangEnv(langEnv)
|
|
14
|
+
if (envLocale) return envLocale
|
|
15
|
+
|
|
16
|
+
// 3. OS detection
|
|
17
|
+
const osLocale = detectOSLocale()
|
|
18
|
+
if (osLocale) return osLocale
|
|
19
|
+
|
|
20
|
+
// 4. Fallback
|
|
21
|
+
return 'en-US'
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function normalizeLocale(raw: string): string | null {
|
|
25
|
+
const lower = raw.toLowerCase().replace('_', '-')
|
|
26
|
+
if (lower.startsWith('zh')) return 'zh-CN'
|
|
27
|
+
if (lower.startsWith('en')) return 'en-US'
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function parseLangEnv(env: string): Locale | null {
|
|
32
|
+
if (!env || env === 'C' || env === 'POSIX') return null
|
|
33
|
+
return normalizeLocale(env) as Locale | null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function detectOSLocale(): Locale | null {
|
|
37
|
+
try {
|
|
38
|
+
if (process.platform === 'darwin') {
|
|
39
|
+
const out = execSync('defaults read -g AppleLocale 2>/dev/null', {
|
|
40
|
+
encoding: 'utf-8',
|
|
41
|
+
timeout: 1000,
|
|
42
|
+
}).trim()
|
|
43
|
+
return normalizeLocale(out) as Locale | null
|
|
44
|
+
}
|
|
45
|
+
if (process.platform === 'win32') {
|
|
46
|
+
// Use Intl as fallback — the kernel32 approach is too complex for startup
|
|
47
|
+
return normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale) as Locale | null
|
|
48
|
+
}
|
|
49
|
+
// Linux: try LANG env one more time, then Intl
|
|
50
|
+
return normalizeLocale(Intl.DateTimeFormat().resolvedOptions().locale) as Locale | null
|
|
51
|
+
} catch {
|
|
52
|
+
return null
|
|
53
|
+
}
|
|
54
|
+
}
|