@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.19.5",
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
  }
@@ -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) || 'Prior conversation context omitted.'
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 || 'User input blocked by hook.',
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 || 'Request blocked by DLP policy.',
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({ role: 'assistant', content: `Error: ${chunk.error}` })
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 { type: 'text', content: `✅ Goal verification passed: ${this.goalVerifyScript}` }
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 { type: 'text', content: `🔄 Verification script failed — continuing (loop ${loop})` }
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 { type: 'text', content: `🔍 Running verification skill: ${this.goalVerifySkill}` }
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: `⚠ Max goal loops (${this.maxGoalLoops}) reached — goal may not be achieved.`,
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 || 'Request blocked by DLP policy.',
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
- `You've reached the maximum of ${MAX_TURNS} tool-calling rounds. ` +
660
- `${toolUses.length} tool call(s) were not executed. ` +
661
- 'Please summarize what you found so far and any next steps the user should take.',
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: `Max tool-calling turns (${MAX_TURNS}) reached. Some tool calls were not executed.`,
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: `Tool "${name}" requires user approval (permission: ask). The tool was not executed.`,
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 || `Tool "${name}" blocked by hook`,
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(`[mcp] Tool collision: "${tool.name}" already registered. Skipping.\n`)
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: `[The Stop hook blocked completion]: ${stopResult.reason || 'Continue working.'}`,
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
+ }