@miphamai/cli 0.19.5 → 0.20.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 +2 -1
- package/src/core/engine.ts +42 -18
- package/src/i18n-context.tsx +33 -0
- 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 +30 -11
- package/src/mcp/client.ts +13 -3
- package/src/mcp/oauth.ts +14 -4
- package/src/mcp/registry.ts +16 -2
- package/src/tools/index.ts +22 -8
- package/src/ui/agent-footer.tsx +3 -1
- package/src/ui/app.tsx +73 -22
- package/src/ui/chat.tsx +31 -21
- package/src/ui/command-picker.tsx +13 -6
- package/src/ui/commands.ts +363 -428
- package/src/ui/input.tsx +53 -101
- package/src/ui/picker.tsx +12 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miphamai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.1",
|
|
4
4
|
"description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@types/bun": "^1.3.14",
|
|
56
56
|
"@types/node": "^22.19.19",
|
|
57
57
|
"@types/react": "^18.3.29",
|
|
58
|
+
"ink-testing-library": "^4.0.0",
|
|
58
59
|
"vitest": "^4.1.7"
|
|
59
60
|
}
|
|
60
61
|
}
|
package/src/core/engine.ts
CHANGED
|
@@ -18,6 +18,16 @@ 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 '../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
|
+
|
|
26
|
+
const bundles: Record<string, TranslationMap> = {
|
|
27
|
+
'en-US': enUS as TranslationMap,
|
|
28
|
+
'zh-CN': zhCN as TranslationMap,
|
|
29
|
+
}
|
|
30
|
+
const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
|
|
21
31
|
|
|
22
32
|
export class QueryEngine {
|
|
23
33
|
private hookEngine?: HookEngine
|
|
@@ -232,7 +242,7 @@ export class QueryEngine {
|
|
|
232
242
|
// Return a minimal summary on failure
|
|
233
243
|
}
|
|
234
244
|
|
|
235
|
-
return summary.slice(0, 8000) || '
|
|
245
|
+
return summary.slice(0, 8000) || t('system.context.omitted')
|
|
236
246
|
})
|
|
237
247
|
}
|
|
238
248
|
|
|
@@ -257,7 +267,7 @@ export class QueryEngine {
|
|
|
257
267
|
if (!submitResult.allowed) {
|
|
258
268
|
yield {
|
|
259
269
|
type: 'error',
|
|
260
|
-
error: submitResult.reason || '
|
|
270
|
+
error: submitResult.reason || t('errors.user_input_blocked'),
|
|
261
271
|
}
|
|
262
272
|
return
|
|
263
273
|
}
|
|
@@ -291,7 +301,7 @@ export class QueryEngine {
|
|
|
291
301
|
if (!verdict.allowed) {
|
|
292
302
|
yield {
|
|
293
303
|
type: 'error',
|
|
294
|
-
error: verdict.reason || '
|
|
304
|
+
error: verdict.reason || t('errors.dlp_blocked'),
|
|
295
305
|
}
|
|
296
306
|
return
|
|
297
307
|
}
|
|
@@ -316,7 +326,10 @@ export class QueryEngine {
|
|
|
316
326
|
yield chunk
|
|
317
327
|
|
|
318
328
|
if (chunk.type === 'error') {
|
|
319
|
-
this.context.addMessage({
|
|
329
|
+
this.context.addMessage({
|
|
330
|
+
role: 'assistant',
|
|
331
|
+
content: t('errors.model_error', { error: chunk.error ?? 'Unknown error' }),
|
|
332
|
+
})
|
|
320
333
|
return
|
|
321
334
|
}
|
|
322
335
|
|
|
@@ -495,17 +508,26 @@ export class QueryEngine {
|
|
|
495
508
|
if (this.goalVerifyScript) {
|
|
496
509
|
const passed = await this.runScriptVerification()
|
|
497
510
|
if (passed) {
|
|
498
|
-
yield {
|
|
511
|
+
yield {
|
|
512
|
+
type: 'text',
|
|
513
|
+
content: t('system.goal.verified', { script: this.goalVerifyScript }),
|
|
514
|
+
}
|
|
499
515
|
break
|
|
500
516
|
}
|
|
501
517
|
// Script failed — continue looping
|
|
502
|
-
yield {
|
|
518
|
+
yield {
|
|
519
|
+
type: 'text',
|
|
520
|
+
content: t('system.goal.failed_continuing', { loop: String(loop) }),
|
|
521
|
+
}
|
|
503
522
|
continue
|
|
504
523
|
}
|
|
505
524
|
|
|
506
525
|
// If using skill verification, delegate to the skill
|
|
507
526
|
if (this.goalVerifySkill) {
|
|
508
|
-
yield {
|
|
527
|
+
yield {
|
|
528
|
+
type: 'text',
|
|
529
|
+
content: t('system.goal.running_skill', { skill: this.goalVerifySkill }),
|
|
530
|
+
}
|
|
509
531
|
yield* this.process(checkMsg, signal)
|
|
510
532
|
if (this.lastAssistantContent?.includes('VERIFIED')) break
|
|
511
533
|
continue
|
|
@@ -519,7 +541,7 @@ export class QueryEngine {
|
|
|
519
541
|
if (loop >= this.maxGoalLoops) {
|
|
520
542
|
yield {
|
|
521
543
|
type: 'text',
|
|
522
|
-
content:
|
|
544
|
+
content: t('system.goal.max_loops', { max: String(this.maxGoalLoops) }),
|
|
523
545
|
}
|
|
524
546
|
}
|
|
525
547
|
}
|
|
@@ -571,7 +593,7 @@ export class QueryEngine {
|
|
|
571
593
|
if (!verdict.allowed) {
|
|
572
594
|
yield {
|
|
573
595
|
type: 'error',
|
|
574
|
-
error: verdict.reason || '
|
|
596
|
+
error: verdict.reason || t('errors.dlp_blocked'),
|
|
575
597
|
}
|
|
576
598
|
return
|
|
577
599
|
}
|
|
@@ -655,10 +677,10 @@ export class QueryEngine {
|
|
|
655
677
|
if (turn === MAX_TURNS - 1 && toolUses.length > 0) {
|
|
656
678
|
this.context.addMessage({
|
|
657
679
|
role: 'user',
|
|
658
|
-
content:
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
680
|
+
content: t('errors.max_tool_turns_warning', {
|
|
681
|
+
max: String(MAX_TURNS),
|
|
682
|
+
pending: String(toolUses.length),
|
|
683
|
+
}),
|
|
662
684
|
})
|
|
663
685
|
// Give model one final chance to respond with a summary
|
|
664
686
|
try {
|
|
@@ -677,7 +699,7 @@ export class QueryEngine {
|
|
|
677
699
|
} catch {
|
|
678
700
|
yield {
|
|
679
701
|
type: 'error',
|
|
680
|
-
error:
|
|
702
|
+
error: t('errors.max_tool_turns', { max: String(MAX_TURNS) }),
|
|
681
703
|
}
|
|
682
704
|
}
|
|
683
705
|
return
|
|
@@ -730,7 +752,7 @@ export class QueryEngine {
|
|
|
730
752
|
return {
|
|
731
753
|
success: false,
|
|
732
754
|
content: '',
|
|
733
|
-
error:
|
|
755
|
+
error: t('errors.tool_not_allowed', { name }),
|
|
734
756
|
}
|
|
735
757
|
}
|
|
736
758
|
|
|
@@ -742,7 +764,7 @@ export class QueryEngine {
|
|
|
742
764
|
return {
|
|
743
765
|
success: false,
|
|
744
766
|
content: '',
|
|
745
|
-
error: preResult.reason ||
|
|
767
|
+
error: preResult.reason || t('errors.tool_blocked', { name }),
|
|
746
768
|
}
|
|
747
769
|
}
|
|
748
770
|
if (preResult.modifiedInput) {
|
|
@@ -803,7 +825,7 @@ export class QueryEngine {
|
|
|
803
825
|
/** Register a tool dynamically (used by MCP auto-registration). */
|
|
804
826
|
registerTool(tool: ToolDefinition): void {
|
|
805
827
|
if (this.tools.has(tool.name)) {
|
|
806
|
-
process.stderr.write(
|
|
828
|
+
process.stderr.write(t('errors.tool_collision', { name: tool.name }) + '\n')
|
|
807
829
|
return
|
|
808
830
|
}
|
|
809
831
|
this.tools.set(tool.name, tool)
|
|
@@ -869,7 +891,9 @@ export class QueryEngine {
|
|
|
869
891
|
// Feed the block reason back to the AI and continue
|
|
870
892
|
this.context.addMessage({
|
|
871
893
|
role: 'user',
|
|
872
|
-
content:
|
|
894
|
+
content: t('system.context.stop_blocked', {
|
|
895
|
+
reason: stopResult.reason || 'Continue working.',
|
|
896
|
+
}),
|
|
873
897
|
})
|
|
874
898
|
yield* this.continueWithTools(signal)
|
|
875
899
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react'
|
|
2
|
+
import type { Locale } from './i18n-core/types'
|
|
3
|
+
|
|
4
|
+
export interface I18nContextValue {
|
|
5
|
+
locale: Locale
|
|
6
|
+
t: (key: string, params?: Record<string, string>) => string
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const I18nContext = createContext<I18nContextValue | null>(null)
|
|
10
|
+
|
|
11
|
+
export function I18nProvider({
|
|
12
|
+
locale,
|
|
13
|
+
t,
|
|
14
|
+
children,
|
|
15
|
+
}: {
|
|
16
|
+
locale: Locale
|
|
17
|
+
t: (key: string, params?: Record<string, string>) => string
|
|
18
|
+
children: React.ReactNode
|
|
19
|
+
}) {
|
|
20
|
+
return React.createElement(I18nContext.Provider, { value: { locale, t } }, children)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function useI18n(): I18nContextValue {
|
|
24
|
+
const ctx = useContext(I18nContext)
|
|
25
|
+
if (!ctx) {
|
|
26
|
+
// If no Provider is mounted (e.g., tests, bare scripts), return en-US no-op.
|
|
27
|
+
return {
|
|
28
|
+
locale: 'en-US',
|
|
29
|
+
t: (key: string) => key,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return ctx
|
|
33
|
+
}
|
|
@@ -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
|
+
}
|