@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.20.0",
3
+ "version": "0.20.2",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -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,
@@ -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 '@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'
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
- type: 'text',
520
- content: t('system.goal.failed_continuing', { loop: String(loop) }),
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 { execSync } = await import('node:child_process')
565
- execSync(this.goalVerifyScript, { timeout: 30000, stdio: 'pipe' })
566
- return true // exit code 0 = success
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
- return { success: false, content: '', error: String(err) }
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 { execSync } from 'node:child_process'
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
- const cmd = [cfg.command, ...args].join(' ')
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
- return { allowed: true }
49
- } catch (err) {
50
- const stderr = (err as { stderr?: string }).stderr || String(err)
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
- // Exit code 2 = block with reason
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
 
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useContext } from 'react'
2
- import type { Locale } from '@mipham/shared/i18n/types'
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
+ }