@miphamai/cli 0.19.4 → 0.20.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/bin/mipham.ts CHANGED
@@ -241,7 +241,7 @@ async function runUpdate(): Promise<boolean> {
241
241
  }
242
242
 
243
243
  if (!latestVersion && lastError) throw lastError
244
- } catch (_err: unknown) {
244
+ } catch {
245
245
  console.log(`✗ Could not reach npm registry (network timeout)`)
246
246
  console.log()
247
247
  console.log(` Manual update:`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.19.4",
3
+ "version": "0.20.0",
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
  }
@@ -1,4 +1,4 @@
1
- import type { ProviderRegistry, ProviderInstance } from '../providers/registry'
1
+ import type { ProviderRegistry } from '../providers/registry'
2
2
  import type { ToolDefinition } from '../shared/index.ts'
3
3
  import type { SubAgentType, SubAgentOptions, AgentDefinition } from './types'
4
4
  import { createAgentContext } from './agent-context'
@@ -30,7 +30,7 @@ const STATUS_HEADERS: Record<string, { label: string; color: string }> = {
30
30
  export function AgentViewDashboard({ manager, onAttach, onExit }: DashboardProps) {
31
31
  const [selectedIndex, setSelectedIndex] = useState(0)
32
32
  const [peekingSessionId, setPeekingSessionId] = useState<string | null>(null)
33
- const [groupBy, setGroupBy] = useState<'status' | 'directory'>('status')
33
+ const [_groupBy, setGroupBy] = useState<'status' | 'directory'>('status')
34
34
 
35
35
  // Build a flat list of sessions in group order, with group headers
36
36
  const flatList = useMemo(() => {
@@ -202,7 +202,7 @@ export function AgentViewDashboard({ manager, onAttach, onExit }: DashboardProps
202
202
  ) : (
203
203
  <Box flexDirection="column">
204
204
  {/* Session list with group headers */}
205
- {flatList.map((item, flatIdx) => {
205
+ {flatList.map((item, _flatIdx) => {
206
206
  if (item.type === 'header') {
207
207
  return (
208
208
  <Box key={`h-${item.status}`} marginY={1}>
@@ -5,8 +5,7 @@
5
5
  * Handlers for: /theme, /release-notes, /ide, /terminal-setup
6
6
  */
7
7
  import type { CommandHandler } from '../ui/commands.js'
8
- import { stripIndent } from '../ui/strip-indent.js'
9
- import { NPM_INSTALL_COMMAND, NPM_UPDATE_COMMAND, PACKAGE_VERSION } from '../shared/index.ts'
8
+ import { NPM_INSTALL_COMMAND } from '../shared/index.ts'
10
9
 
11
10
  export { themeCmd, releaseNotesCmd, ideCmd, terminalSetupCmd }
12
11
 
@@ -89,7 +88,7 @@ Full changelog: https://mipham.ai/code/releases`,
89
88
  // ═══════════════════════════════════════════════════════════════
90
89
 
91
90
  const ideCmd: CommandHandler = async (_ctx) => {
92
- const { mkdirSync, writeFileSync, existsSync } = await import('node:fs')
91
+ const { mkdirSync, writeFileSync } = await import('node:fs')
93
92
  const { join } = await import('node:path')
94
93
 
95
94
  const cwd = process.cwd()
@@ -5,7 +5,6 @@
5
5
  * Handlers for: /commit, /push, /pr, /issue
6
6
  */
7
7
  import type { CommandHandler } from '../ui/commands.js'
8
- import { stripIndent } from '../ui/strip-indent.js'
9
8
 
10
9
  export { commitCmd, pushCmd, prCmd, issueCmd }
11
10
 
@@ -6,7 +6,6 @@
6
6
  * /security, /audit, /prompt-audit
7
7
  */
8
8
  import type { CommandHandler, CommandContext, CommandResult } from '../ui/commands.js'
9
- import { stripIndent } from '../ui/strip-indent.js'
10
9
 
11
10
  export { initCmd, permissionsCmd, recommendCmd, setupCmd, addDirCmd, promptAuditCmd, securityCmd }
12
11
 
@@ -135,10 +134,6 @@ const recommendCmd: CommandHandler = async (ctx) => {
135
134
  const hasRequirements = existsSync(join(cwd, 'requirements.txt'))
136
135
  const hasDockerfile = existsSync(join(cwd, 'Dockerfile'))
137
136
  const hasGitHubActions = existsSync(join(cwd, '.github', 'workflows'))
138
- const hasNextConfig =
139
- existsSync(join(cwd, 'next.config.js')) || existsSync(join(cwd, 'next.config.ts'))
140
- const hasVueConfig =
141
- existsSync(join(cwd, 'vite.config.ts')) || existsSync(join(cwd, 'vite.config.js'))
142
137
  const hasTailwind =
143
138
  existsSync(join(cwd, 'tailwind.config.ts')) || existsSync(join(cwd, 'tailwind.config.js'))
144
139
 
@@ -704,7 +699,7 @@ Permission level is controlled by /config permission <level>.`,
704
699
 
705
700
  const promptAuditCmd: CommandHandler = async () => {
706
701
  const { existsSync, readFileSync, readdirSync, statSync } = await import('node:fs')
707
- const { join, extname, basename } = await import('node:path')
702
+ const { join, basename } = await import('node:path')
708
703
  const cwd = process.cwd()
709
704
 
710
705
  // Patterns that suggest prompts written for older/less-capable models
@@ -7,7 +7,7 @@ import {
7
7
  unlinkSync,
8
8
  chmodSync,
9
9
  } from 'node:fs'
10
- import { join, dirname } from 'node:path'
10
+ import { join } from 'node:path'
11
11
  import { homedir } from 'node:os'
12
12
  import { parse as parseYaml } from 'yaml'
13
13
  import type {
@@ -1,4 +1,3 @@
1
- import { relative, isAbsolute } from 'node:path'
2
1
  import { homedir } from 'node:os'
3
2
  import type { CredentialMaskingConfig, CredentialFileRule } from '../shared/index.ts'
4
3
 
@@ -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 '@mipham/shared/i18n/t'
22
+ import enUS from '@mipham/shared/i18n/locales/en-US.json'
23
+ import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
24
+ import type { TranslationMap } from '@mipham/shared/i18n/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
  }
package/src/core/hooks.ts CHANGED
@@ -4,7 +4,6 @@ import type {
4
4
  HookContext,
5
5
  HookResult,
6
6
  ToolResult,
7
- HookConfig,
8
7
  } from '../shared/index.ts'
9
8
 
10
9
  /**
@@ -18,7 +18,7 @@
18
18
  */
19
19
 
20
20
  import { readdirSync, readFileSync, existsSync } from 'node:fs'
21
- import { join, relative } from 'node:path'
21
+ import { join } from 'node:path'
22
22
 
23
23
  interface RuleFile {
24
24
  name: string
@@ -146,7 +146,7 @@ export class RulesLoader {
146
146
  */
147
147
  private parseRule(
148
148
  raw: string,
149
- filename: string,
149
+ _filename: string,
150
150
  ): { paths: string[]; description: string; content: string } {
151
151
  const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/)
152
152
  if (!match) {
@@ -0,0 +1,37 @@
1
+ import React, { createContext, useContext } from 'react'
2
+ import type { Locale } from '@mipham/shared/i18n/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(
21
+ I18nContext.Provider,
22
+ { value: { locale, t } },
23
+ children,
24
+ )
25
+ }
26
+
27
+ export function useI18n(): I18nContextValue {
28
+ const ctx = useContext(I18nContext)
29
+ if (!ctx) {
30
+ // If no Provider is mounted (e.g., tests, bare scripts), return en-US no-op.
31
+ return {
32
+ locale: 'en-US',
33
+ t: (key: string) => key,
34
+ }
35
+ }
36
+ return ctx
37
+ }
package/src/index.tsx CHANGED
@@ -1,3 +1,4 @@
1
+ import React from 'react'
1
2
  import { join } from 'node:path'
2
3
  import { existsSync } from 'node:fs'
3
4
  import { render } from 'ink'
@@ -23,6 +24,12 @@ import { getMetrics } from './core/metrics'
23
24
  import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
24
25
  import { AgentViewManager } from './agent-view/agent-view-manager'
25
26
  import { AgentViewDashboard } from './agent-view/dashboard'
27
+ import { createT } from '@mipham/shared/i18n/t'
28
+ import { detectLocale } from '@mipham/shared/i18n/detect'
29
+ import { I18nProvider } from './i18n-context'
30
+ import enUS from '@mipham/shared/i18n/locales/en-US.json' with { type: 'json' }
31
+ import zhCN from '@mipham/shared/i18n/locales/zh-CN.json' with { type: 'json' }
32
+ import type { TranslationMap } from '@mipham/shared/i18n/types'
26
33
 
27
34
  interface RunOptions {
28
35
  model?: string
@@ -60,6 +67,11 @@ export async function runApp(options: RunOptions): Promise<void> {
60
67
  // Load configuration
61
68
  const config = loadConfig()
62
69
 
70
+ // Detect locale and create translation function
71
+ const locale = detectLocale({ lang: options.lang })
72
+ const localeBundles: Record<string, TranslationMap> = { 'en-US': enUS as TranslationMap, 'zh-CN': zhCN as TranslationMap }
73
+ const t = createT(localeBundles[locale] || enUS, enUS)
74
+
63
75
  // Bootstrap providers
64
76
  const defaultProvider = options.provider || config.defaultProvider
65
77
  const defaultModel = options.model || config.defaultModel
@@ -297,17 +309,21 @@ export async function runApp(options: RunOptions): Promise<void> {
297
309
  })
298
310
 
299
311
  const { waitUntilExit } = render(
300
- <App
301
- engine={engine}
302
- config={config}
303
- initialProvider={defaultProvider}
304
- initialModel={defaultModel}
305
- lang={options.lang}
306
- skillsLoader={skillsLoader}
307
- pluginManager={pluginManager}
308
- version={options.version}
309
- sessionId={sessionName}
310
- />,
312
+ React.createElement(I18nProvider, {
313
+ locale,
314
+ t,
315
+ children: React.createElement(App, {
316
+ engine,
317
+ config,
318
+ initialProvider: defaultProvider,
319
+ initialModel: defaultModel,
320
+ lang: options.lang,
321
+ skillsLoader,
322
+ pluginManager,
323
+ version: options.version,
324
+ sessionId: sessionName,
325
+ }),
326
+ }),
311
327
  )
312
328
  await waitUntilExit()
313
329
  saveAndExit()
package/src/mcp/client.ts CHANGED
@@ -10,6 +10,16 @@ import { StdioTransport } from './transport'
10
10
  import { McpProtocol } from './protocol'
11
11
  import { OAuthClient } from './oauth'
12
12
  import { TokenStore } from './token-store'
13
+ import { createT } from '@mipham/shared/i18n/t'
14
+ import enUS from '@mipham/shared/i18n/locales/en-US.json'
15
+ import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
16
+ import type { TranslationMap } from '@mipham/shared/i18n/types'
17
+
18
+ const bundles: Record<string, TranslationMap> = {
19
+ 'en-US': enUS as TranslationMap,
20
+ 'zh-CN': zhCN as TranslationMap,
21
+ }
22
+ const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
13
23
 
14
24
  interface ActiveConnection {
15
25
  config: McpServerConfig
@@ -98,7 +108,7 @@ export class McpClient {
98
108
  /** Reconnect with exponential backoff (1s→2s→4s→…max 60s, 10 attempts). */
99
109
  async reconnect(name: string): Promise<void> {
100
110
  const connection = this.connections.get(name)
101
- if (!connection) throw new Error(`No connection for "${name}"`)
111
+ if (!connection) throw new Error(t('errors.mcp_no_connection', { name }))
102
112
 
103
113
  const config = connection.config
104
114
  let delay = 1000
@@ -262,7 +272,7 @@ export class McpClient {
262
272
  const conn = this.connections.get(serverName)
263
273
  if (!conn || conn.status !== 'connected') {
264
274
  return {
265
- content: [{ type: 'text', text: `Error: MCP server "${serverName}" not connected` }],
275
+ content: [{ type: 'text', text: t('errors.mcp_not_connected', { server: serverName }) }],
266
276
  isError: true,
267
277
  }
268
278
  }
@@ -271,7 +281,7 @@ export class McpClient {
271
281
  return await conn.protocol.callTool(toolName, params)
272
282
  } catch (err) {
273
283
  return {
274
- content: [{ type: 'text', text: `MCP tool error: ${String(err)}` }],
284
+ content: [{ type: 'text', text: t('errors.mcp_tool_error', { error: String(err) }) }],
275
285
  isError: true,
276
286
  }
277
287
  }
package/src/mcp/oauth.ts CHANGED
@@ -4,6 +4,16 @@ import { createServer, Server } from 'node:http'
4
4
  import type { IncomingMessage, ServerResponse } from 'node:http'
5
5
  import type { McpServerConfig } from '../shared/types'
6
6
  import { TokenStore } from './token-store'
7
+ import { createT } from '@mipham/shared/i18n/t'
8
+ import enUS from '@mipham/shared/i18n/locales/en-US.json'
9
+ import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
10
+ import type { TranslationMap } from '@mipham/shared/i18n/types'
11
+
12
+ const bundles: Record<string, TranslationMap> = {
13
+ 'en-US': enUS as TranslationMap,
14
+ 'zh-CN': zhCN as TranslationMap,
15
+ }
16
+ const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
7
17
 
8
18
  interface TokenResponse {
9
19
  accessToken: string
@@ -41,18 +51,18 @@ export class OAuthClient {
41
51
  if (receivedState !== state) {
42
52
  res.writeHead(400)
43
53
  res.end('State mismatch')
44
- reject(new Error('OAuth state mismatch — possible CSRF'))
54
+ reject(new Error(t('errors.oauth_state_mismatch')))
45
55
  return
46
56
  }
47
57
  if (!receivedCode) {
48
58
  res.writeHead(400)
49
59
  res.end('No code received')
50
- reject(new Error('No authorization code received'))
60
+ reject(new Error(t('errors.oauth_no_code')))
51
61
  return
52
62
  }
53
63
  res.writeHead(200, { 'Content-Type': 'text/html' })
54
64
  res.end(
55
- '<html><body><h1>Authenticated</h1><p>You may close this window.</p></body></html>',
65
+ `<html><body><h1>${t('system.oauth.authenticated')}</h1><p>${t('system.oauth.close_window')}</p></body></html>`,
56
66
  )
57
67
  server.close()
58
68
  resolve(receivedCode)
@@ -81,7 +91,7 @@ export class OAuthClient {
81
91
  })
82
92
  setTimeout(() => {
83
93
  server.close()
84
- reject(new Error('OAuth flow timed out (5 minutes)'))
94
+ reject(new Error(t('errors.oauth_timeout')))
85
95
  }, 300_000)
86
96
  })
87
97
 
@@ -9,6 +9,16 @@
9
9
  import type { ToolDefinition as McpToolDefinition, ToolCallResult } from './types'
10
10
  import type { ToolDefinition, ToolResult, ToolContext } from '../shared/types'
11
11
  import { McpClient } from './client'
12
+ import { createT } from '@mipham/shared/i18n/t'
13
+ import enUS from '@mipham/shared/i18n/locales/en-US.json'
14
+ import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
15
+ import type { TranslationMap } from '@mipham/shared/i18n/types'
16
+
17
+ const bundles: Record<string, TranslationMap> = {
18
+ 'en-US': enUS as TranslationMap,
19
+ 'zh-CN': zhCN as TranslationMap,
20
+ }
21
+ const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
12
22
 
13
23
  const MCP_TOOL_PREFIX = 'mcp__'
14
24
  const MAX_NAME_LENGTH = 128
@@ -108,7 +118,7 @@ export function registerMcpServerTools(
108
118
 
109
119
  if (toolsMap.has(tool.name)) {
110
120
  process.stderr.write(
111
- `[mcp] Tool collision: "${tool.name}" (from server "${serverName}") conflicts with existing tool. Skipping.\n`,
121
+ t('errors.mcp_register_collision', { name: tool.name, server: serverName }) + '\n',
112
122
  )
113
123
  continue
114
124
  }
@@ -117,7 +127,7 @@ export function registerMcpServerTools(
117
127
  registered++
118
128
  } catch (err) {
119
129
  process.stderr.write(
120
- `[mcp] Failed to register tool "${mcpTool.name}" from server "${serverName}": ${String(err)}\n`,
130
+ t('errors.mcp_register_failed', { tool: mcpTool.name, server: serverName, error: String(err) }) + '\n',
121
131
  )
122
132
  }
123
133
  }
@@ -5,7 +5,7 @@
5
5
  * from remote sources (GitHub repos, direct URLs).
6
6
  */
7
7
 
8
- import { existsSync, mkdirSync, writeFileSync, readdirSync, unlinkSync, chmodSync } from 'node:fs'
8
+ import { existsSync, mkdirSync, writeFileSync, readdirSync, unlinkSync } from 'node:fs'
9
9
  import { join } from 'node:path'
10
10
  import { homedir } from 'node:os'
11
11
  import { spawnSync } from 'node:child_process'
@@ -1,4 +1,4 @@
1
- import type { ToolDefinition, SkillDefinition, ToolResult } from '../../shared/index.ts'
1
+ import type { ToolDefinition } from '../../shared/index.ts'
2
2
  import { executeForkedSkill } from '../../skills/fork-executor'
3
3
 
4
4
  export const skillTool: ToolDefinition = {
@@ -129,7 +129,7 @@ export const enterWorktreeTool: ToolDefinition = {
129
129
  ['git', 'worktree', 'add', '-b', branchName, worktreePath, resolvedBaseRef],
130
130
  { cwd, stdout: 'pipe', stderr: 'pipe' },
131
131
  )
132
- const stdout = await new Response(proc.stdout).text()
132
+ const _stdout = await new Response(proc.stdout).text()
133
133
  const exitCode = await proc.exited
134
134
 
135
135
  if (exitCode !== 0) {
@@ -1,5 +1,9 @@
1
1
  import type { ToolDefinition, ToolResult } from '../shared/index.ts'
2
2
  import { sanitizeParams } from '../shared/sanitize'
3
+ import { createT } from '@mipham/shared/i18n/t'
4
+ import enUS from '@mipham/shared/i18n/locales/en-US.json'
5
+ import zhCN from '@mipham/shared/i18n/locales/zh-CN.json'
6
+ import type { TranslationMap } from '@mipham/shared/i18n/types'
3
7
  import { readTool } from './file/read'
4
8
  import { writeTool } from './file/write'
5
9
  import { editTool } from './file/edit'
@@ -29,6 +33,12 @@ import { computerUseTool } from './computer/computer-use'
29
33
  import { scheduleWakeupTool } from './scheduling/schedule-wakeup.js'
30
34
  import { cronCreateTool, cronDeleteTool, cronListTool } from './scheduling/cron.js'
31
35
 
36
+ const bundles: Record<string, TranslationMap> = {
37
+ 'en-US': enUS as TranslationMap,
38
+ 'zh-CN': zhCN as TranslationMap,
39
+ }
40
+ const t = createT(bundles['en-US'] || (enUS as TranslationMap), enUS as TranslationMap)
41
+
32
42
  /**
33
43
  * Validate tool parameters against the tool's JSON Schema definition.
34
44
  * Returns an array of error messages (empty = valid).
@@ -44,7 +54,7 @@ function validateParams(
44
54
  if (required) {
45
55
  for (const field of required) {
46
56
  if (params[field] === undefined || params[field] === null) {
47
- errors.push(`Missing required parameter: "${field}"`)
57
+ errors.push(t('errors.missing_param', { param: field }))
48
58
  }
49
59
  }
50
60
  }
@@ -59,25 +69,25 @@ function validateParams(
59
69
 
60
70
  switch (def.type) {
61
71
  case 'string':
62
- if (typeof value !== 'string') errors.push(`"${key}" must be a string`)
72
+ if (typeof value !== 'string') errors.push(t('errors.type_string', { key }))
63
73
  else if (def.enum && !def.enum.includes(value)) {
64
- errors.push(`"${key}" must be one of: ${def.enum.join(', ')}`)
74
+ errors.push(t('errors.type_enum', { key, values: def.enum.join(', ') }))
65
75
  }
66
76
  break
67
77
  case 'integer':
68
78
  case 'number':
69
- if (typeof value !== 'number') errors.push(`"${key}" must be a number`)
79
+ if (typeof value !== 'number') errors.push(t('errors.type_number', { key }))
70
80
  break
71
81
  case 'boolean':
72
- if (typeof value !== 'boolean') errors.push(`"${key}" must be a boolean`)
82
+ if (typeof value !== 'boolean') errors.push(t('errors.type_boolean', { key }))
73
83
  break
74
84
  case 'object':
75
85
  if (typeof value !== 'object' || Array.isArray(value)) {
76
- errors.push(`"${key}" must be an object`)
86
+ errors.push(t('errors.type_object', { key }))
77
87
  }
78
88
  break
79
89
  case 'array':
80
- if (!Array.isArray(value)) errors.push(`"${key}" must be an array`)
90
+ if (!Array.isArray(value)) errors.push(t('errors.type_array', { key }))
81
91
  break
82
92
  }
83
93
  }
@@ -100,7 +110,7 @@ function withValidation(tool: ToolDefinition): ToolDefinition {
100
110
  const cleanParams = sanitizeParams(params)
101
111
  const errors = validateParams(schema, cleanParams)
102
112
  if (errors.length > 0) {
103
- return { success: false, content: '', error: `Invalid parameters: ${errors.join('; ')}` }
113
+ return { success: false, content: '', error: t('errors.invalid_params', { errors: errors.join('; ') }) }
104
114
  }
105
115
  return tool.execute(cleanParams, ctx)
106
116
  },