@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 +1 -1
- package/package.json +2 -1
- package/src/agent/sub-agent.ts +1 -1
- package/src/agent-view/dashboard.tsx +2 -2
- package/src/commands/environment.ts +2 -3
- package/src/commands/git.ts +0 -1
- package/src/commands/project.ts +1 -6
- package/src/config/loader.ts +1 -1
- package/src/core/credential-masker.ts +0 -1
- package/src/core/engine.ts +42 -18
- package/src/core/hooks.ts +0 -1
- package/src/core/rules-loader.ts +2 -2
- package/src/i18n-context.tsx +37 -0
- package/src/index.tsx +27 -11
- package/src/mcp/client.ts +13 -3
- package/src/mcp/oauth.ts +14 -4
- package/src/mcp/registry.ts +12 -2
- package/src/skills/registry.ts +1 -1
- package/src/tools/agent/skill.ts +1 -1
- package/src/tools/exec/enter-worktree.ts +1 -1
- package/src/tools/index.ts +18 -8
- package/src/ui/agent-footer.tsx +3 -1
- package/src/ui/app.tsx +22 -16
- package/src/ui/chat.tsx +19 -15
- package/src/ui/command-picker.tsx +13 -8
- package/src/ui/commands.ts +310 -434
- package/src/ui/input.tsx +55 -103
- package/src/ui/picker.tsx +10 -8
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
|
|
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.
|
|
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
|
}
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProviderRegistry
|
|
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 [
|
|
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,
|
|
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 {
|
|
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
|
|
91
|
+
const { mkdirSync, writeFileSync } = await import('node:fs')
|
|
93
92
|
const { join } = await import('node:path')
|
|
94
93
|
|
|
95
94
|
const cwd = process.cwd()
|
package/src/commands/git.ts
CHANGED
package/src/commands/project.ts
CHANGED
|
@@ -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,
|
|
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
|
package/src/config/loader.ts
CHANGED
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 '@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) || '
|
|
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
|
}
|
package/src/core/hooks.ts
CHANGED
package/src/core/rules-loader.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { readdirSync, readFileSync, existsSync } from 'node:fs'
|
|
21
|
-
import { join
|
|
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
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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(
|
|
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:
|
|
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:
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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(
|
|
94
|
+
reject(new Error(t('errors.oauth_timeout')))
|
|
85
95
|
}, 300_000)
|
|
86
96
|
})
|
|
87
97
|
|
package/src/mcp/registry.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
130
|
+
t('errors.mcp_register_failed', { tool: mcpTool.name, server: serverName, error: String(err) }) + '\n',
|
|
121
131
|
)
|
|
122
132
|
}
|
|
123
133
|
}
|
package/src/skills/registry.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* from remote sources (GitHub repos, direct URLs).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync, mkdirSync, writeFileSync, readdirSync, unlinkSync
|
|
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'
|
package/src/tools/agent/skill.ts
CHANGED
|
@@ -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
|
|
132
|
+
const _stdout = await new Response(proc.stdout).text()
|
|
133
133
|
const exitCode = await proc.exited
|
|
134
134
|
|
|
135
135
|
if (exitCode !== 0) {
|
package/src/tools/index.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
86
|
+
errors.push(t('errors.type_object', { key }))
|
|
77
87
|
}
|
|
78
88
|
break
|
|
79
89
|
case 'array':
|
|
80
|
-
if (!Array.isArray(value)) errors.push(
|
|
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:
|
|
113
|
+
return { success: false, content: '', error: t('errors.invalid_params', { errors: errors.join('; ') }) }
|
|
104
114
|
}
|
|
105
115
|
return tool.execute(cleanParams, ctx)
|
|
106
116
|
},
|