@miphamai/cli 0.44.4 → 0.45.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.44.4",
3
+ "version": "0.45.0",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -86,6 +86,8 @@ export class QueryEngine {
86
86
  private goal?: string
87
87
  private maxGoalLoops = 20
88
88
  private lastAssistantContent?: string
89
+ /** Reasoning effort (low|medium|high|xhigh|max) — scales the provider idle timeout. */
90
+ private effort: string = 'high'
89
91
  /** Custom verification shell script path for goal checking. */
90
92
  private goalVerifyScript?: string
91
93
  /** Custom verification skill name for goal checking. */
@@ -117,6 +119,11 @@ export class QueryEngine {
117
119
  dialogExpiry: 300,
118
120
  }
119
121
 
122
+ /** Set the reasoning effort (low|medium|high|xhigh|max). */
123
+ setEffort(level: string): void {
124
+ this.effort = level
125
+ }
126
+
120
127
  /** Set the session identifier (called from index.tsx at startup with the actual session name). */
121
128
  setSessionId(id: string): void {
122
129
  this.sessionId = id
@@ -1209,6 +1216,7 @@ export class QueryEngine {
1209
1216
  systemPrompt,
1210
1217
  tools: toolDefs,
1211
1218
  signal,
1219
+ effort: this.effort,
1212
1220
  })) {
1213
1221
  if (chunk.type === 'error') {
1214
1222
  failure = chunk.error ?? 'Unknown error'
@@ -1253,6 +1261,7 @@ export class QueryEngine {
1253
1261
  systemPrompt,
1254
1262
  tools: toolDefs,
1255
1263
  signal,
1264
+ effort: this.effort,
1256
1265
  })) {
1257
1266
  yield chunk
1258
1267
  }
@@ -224,6 +224,9 @@ export class RemoteEngine {
224
224
  /** No-op: provider switching is managed by the daemon session. */
225
225
  switchProvider(_providerId: string, _modelId?: string): void {}
226
226
 
227
+ /** No-op: reasoning effort is managed by the daemon session. */
228
+ setEffort(_level: string): void {}
229
+
227
230
  /** Remote mode has no local agent registry. */
228
231
  getAgentRegistry(): undefined {
229
232
  return undefined
@@ -6,7 +6,7 @@ import type {
6
6
  ContentBlock,
7
7
  } from '../shared/index.ts'
8
8
  import type { ProviderInstance, ChatRequest } from './registry'
9
- import { fetchWithRetry } from './fetch-utils'
9
+ import { fetchWithRetry, streamIdleTimeoutMs } from './fetch-utils'
10
10
 
11
11
  interface AnthropicContentBlock {
12
12
  type: string
@@ -109,24 +109,33 @@ export class AnthropicProvider implements ProviderInstance {
109
109
  const decoder = new TextDecoder()
110
110
  let buffer = ''
111
111
 
112
- // Streaming read timeout: if no data arrives for 90s, abort to prevent UI freeze.
113
- const STREAM_READ_TIMEOUT_MS = 90_000
112
+ // Streaming idle timeout: scaled by reasoning effort so extended thinking
113
+ // passes aren't mistaken for a stalled connection.
114
+ const STREAM_READ_TIMEOUT_MS = streamIdleTimeoutMs(req.effort)
114
115
 
115
116
  while (true) {
116
117
  let readResult: Awaited<ReturnType<typeof reader.read>>
118
+ let idleTimer: ReturnType<typeof setTimeout> | undefined
117
119
  try {
118
120
  readResult = await Promise.race([
119
121
  reader.read(),
120
- new Promise<never>((_, reject) =>
121
- setTimeout(
122
- () => reject(new Error('Stream read timeout — no data for 90s')),
122
+ new Promise<never>((_, reject) => {
123
+ idleTimer = setTimeout(
124
+ () =>
125
+ reject(
126
+ new Error(
127
+ `Stream read timeout — no data for ${Math.round(STREAM_READ_TIMEOUT_MS / 1000)}s`,
128
+ ),
129
+ ),
123
130
  STREAM_READ_TIMEOUT_MS,
124
- ),
125
- ),
131
+ )
132
+ }),
126
133
  ])
127
134
  } catch (err) {
128
135
  yield { type: 'error', error: `Stream stalled: ${String(err)}` }
129
136
  return
137
+ } finally {
138
+ if (idleTimer) clearTimeout(idleTimer)
130
139
  }
131
140
  const { done, value } = readResult
132
141
  if (done) break
@@ -80,6 +80,28 @@ function sleep(ms: number): Promise<void> {
80
80
  return new Promise((resolve) => setTimeout(resolve, ms))
81
81
  }
82
82
 
83
+ // ── Streaming idle-timeout (stall guard) with effort multiplier ──────────
84
+ //
85
+ // Reasoning models think silently for long stretches. The idle timeout between
86
+ // stream chunks is scaled by reasoning effort so a long thinking pass isn't
87
+ // mistaken for a stalled connection (jcode stall-guard alignment).
88
+
89
+ /** Base idle timeout between stream chunks, in ms. */
90
+ export const STREAM_IDLE_TIMEOUT_BASE_MS = 90_000
91
+
92
+ /** Effort level → idle-timeout multiplier. Unknown/absent effort → 1× (base). */
93
+ const EFFORT_TIMEOUT_MULTIPLIER: Record<string, number> = {
94
+ high: 2,
95
+ xhigh: 3,
96
+ max: 4,
97
+ }
98
+
99
+ /** Compute the streaming idle timeout for a given reasoning-effort level. */
100
+ export function streamIdleTimeoutMs(effort?: string): number {
101
+ const multiplier = effort ? (EFFORT_TIMEOUT_MULTIPLIER[effort] ?? 1) : 1
102
+ return STREAM_IDLE_TIMEOUT_BASE_MS * multiplier
103
+ }
104
+
83
105
  /**
84
106
  * Combine multiple AbortSignals into one — any signal aborting
85
107
  * triggers the combined signal.
@@ -1,7 +1,7 @@
1
1
  import { execSync } from 'node:child_process'
2
2
  import type { ProviderConfig, ModelInfo, Message, StreamChunk } from '../shared/index.ts'
3
3
  import type { ProviderInstance, ChatRequest } from './registry'
4
- import { fetchWithRetry } from './fetch-utils'
4
+ import { fetchWithRetry, streamIdleTimeoutMs } from './fetch-utils'
5
5
  import { OLLAMA_PRESET_MODELS } from '../shared/constants'
6
6
 
7
7
  export class OpenAICompatProvider implements ProviderInstance {
@@ -54,25 +54,33 @@ export class OpenAICompatProvider implements ProviderInstance {
54
54
  const pendingToolCalls = new Map<number, { id: string; name: string; arguments: string }>()
55
55
  let reasoningContent = ''
56
56
 
57
- // Streaming read timeout: if no data arrives for 90s, abort to prevent UI freeze.
58
- // DeepSeek V4 thinking mode can take 30-60s between chunks — 90s is a generous ceiling.
59
- const STREAM_READ_TIMEOUT_MS = 90_000
57
+ // Streaming idle timeout: scaled by reasoning effort so long thinking passes
58
+ // (DeepSeek V4 / reasoning models) aren't mistaken for a stalled connection.
59
+ const STREAM_READ_TIMEOUT_MS = streamIdleTimeoutMs(req.effort)
60
60
 
61
61
  while (true) {
62
62
  let readResult: Awaited<ReturnType<typeof reader.read>>
63
+ let idleTimer: ReturnType<typeof setTimeout> | undefined
63
64
  try {
64
65
  readResult = await Promise.race([
65
66
  reader.read(),
66
- new Promise<never>((_, reject) =>
67
- setTimeout(
68
- () => reject(new Error('Stream read timeout — no data for 90s')),
67
+ new Promise<never>((_, reject) => {
68
+ idleTimer = setTimeout(
69
+ () =>
70
+ reject(
71
+ new Error(
72
+ `Stream read timeout — no data for ${Math.round(STREAM_READ_TIMEOUT_MS / 1000)}s`,
73
+ ),
74
+ ),
69
75
  STREAM_READ_TIMEOUT_MS,
70
- ),
71
- ),
76
+ )
77
+ }),
72
78
  ])
73
79
  } catch (err) {
74
80
  yield { type: 'error', error: `Stream stalled: ${String(err)}` }
75
81
  return
82
+ } finally {
83
+ if (idleTimer) clearTimeout(idleTimer)
76
84
  }
77
85
  const { done, value } = readResult
78
86
  if (done) break
@@ -17,6 +17,8 @@ export interface ChatRequest {
17
17
  maxTokens?: number
18
18
  temperature?: number
19
19
  signal?: AbortSignal
20
+ /** Reasoning effort (low|medium|high|xhigh|max) — scales the streaming idle timeout. */
21
+ effort?: string
20
22
  }
21
23
 
22
24
  export class ProviderRegistry implements Llm {
@@ -7,6 +7,30 @@ import { realpathSync, existsSync } from 'node:fs'
7
7
  */
8
8
  const BLOCKED_PATHS = ['/etc', '/proc', '/sys', '/dev', '/boot', '/root']
9
9
 
10
+ /**
11
+ * Windows UNC paths and NT/Win32/DOS device-namespace prefixes.
12
+ *
13
+ * Accessing a UNC path (\\server\share) triggers SMB negotiation on Windows,
14
+ * which silently sends NTLM credentials to the target — a credential-leak
15
+ * vector (relay / offline cracking). The NT-namespace spelling (\??\UNC\...)
16
+ * bypasses naive UNC checks, so all device-namespace prefixes are rejected too.
17
+ *
18
+ * Anchored at token boundaries so escaped backslashes (echo \\n) and local
19
+ * drive paths (C:\\Users\\me) are NOT flagged, and http:// URLs are excluded
20
+ * from the forward-slash form.
21
+ */
22
+ const UNC_DEVICE_PATTERNS: RegExp[] = [
23
+ /(?:^|[\s"'`(;|&])\\\\[A-Za-z0-9._-]+\\[^\s"'`;|&]/,
24
+ /(?:^|[\s"'`(;|&])\/\/[A-Za-z0-9._-]+\/[^\s"'`;|&]/,
25
+ /\\\?\?\\/,
26
+ /\\\\\?\\/,
27
+ /\\\\\.\\/,
28
+ ]
29
+
30
+ export function isUncOrDevicePath(path: string): boolean {
31
+ return UNC_DEVICE_PATTERNS.some((re) => re.test(path))
32
+ }
33
+
10
34
  /**
11
35
  * Resolve a user-supplied path relative to cwd, with sandbox enforcement.
12
36
  *
@@ -22,6 +46,14 @@ const BLOCKED_PATHS = ['/etc', '/proc', '/sys', '/dev', '/boot', '/root']
22
46
  * @throws {Error} with a human-readable message if the path is blocked.
23
47
  */
24
48
  export function resolveSafe(cwd: string, inputPath: string): string {
49
+ // Reject UNC / device-namespace paths before touching the filesystem —
50
+ // resolving a UNC path on Windows would trigger SMB negotiation (NTLM leak).
51
+ if (isUncOrDevicePath(inputPath)) {
52
+ throw new Error(
53
+ `Path rejected: "${inputPath}" is a UNC or device-namespace path (blocked to prevent NTLM credential leakage).`,
54
+ )
55
+ }
56
+
25
57
  const raw = resolve(cwd, inputPath)
26
58
  const normalized = normalize(raw)
27
59
 
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.44.4' as const
12
+ export const PACKAGE_VERSION = '0.45.0' as const
13
13
 
14
14
  /** npm install 全局安装命令 */
15
15
  export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
@@ -54,9 +54,12 @@ export class MiphamRuntime {
54
54
  ...variables,
55
55
  }
56
56
 
57
- for (const [key, value] of Object.entries(allVars)) {
58
- result = result.replaceAll(`\${${key}}`, value)
59
- }
57
+ // Single-pass substitution: replacement values are NOT re-scanned for more
58
+ // template markers, so a variable containing `${key}` can't be re-expanded
59
+ // into another variable's value.
60
+ result = result.replace(/\$\{([^}]+)\}/g, (match, key: string) =>
61
+ Object.prototype.hasOwnProperty.call(allVars, key) ? allVars[key]! : match,
62
+ )
60
63
 
61
64
  return result
62
65
  }
@@ -49,9 +49,12 @@ export class StandardRuntime {
49
49
 
50
50
  let result = prompt
51
51
  if (variables) {
52
- for (const [key, value] of Object.entries(variables)) {
53
- result = result.replaceAll(`\${${key}}`, value)
54
- }
52
+ // Single-pass substitution: replacement values are NOT re-scanned for
53
+ // more template markers, so an argument containing `${key}` can't be
54
+ // re-expanded into another variable's value.
55
+ result = result.replace(/\$\{([^}]+)\}/g, (match, key: string) =>
56
+ Object.prototype.hasOwnProperty.call(variables, key) ? variables[key]! : match,
57
+ )
55
58
  }
56
59
 
57
60
  return result
@@ -1,6 +1,7 @@
1
1
  import type { ToolDefinition, CredentialMaskingConfig } from '../../shared/index.ts'
2
2
  import { sanitizeCommand } from '../../shared/sanitize.ts'
3
3
  import { DANGEROUS_GIT_PATTERNS } from './git.ts'
4
+ import { isUncOrDevicePath } from '../../security/path.ts'
4
5
  import type { Service } from '../../vajra'
5
6
  import { toolKey } from '../seam'
6
7
  import { withValidation } from '../validation'
@@ -137,6 +138,12 @@ export function isBlocked(command: string): string | null {
137
138
  }
138
139
  }
139
140
 
141
+ // Reject UNC / device-namespace paths — accessing one triggers SMB negotiation
142
+ // on Windows and silently leaks NTLM credentials (Claude Code 2.1.233 CVE-class fix).
143
+ if (isUncOrDevicePath(command) || isUncOrDevicePath(normalized) || isUncOrDevicePath(sanitized)) {
144
+ return `Command rejected: references a UNC or device-namespace path (blocked to prevent NTLM credential leakage).`
145
+ }
146
+
140
147
  // Check dangerous patterns (on original, normalized, and sanitized)
141
148
  for (const pattern of BLOCKED_PATTERNS) {
142
149
  if (pattern.test(command) || pattern.test(normalized) || pattern.test(sanitized)) {
package/src/ui/app.tsx CHANGED
@@ -278,6 +278,7 @@ export function App({
278
278
  setEffort: (level: string) => {
279
279
  setEffort(level)
280
280
  setPreference('lastCodeReviewEffort', level)
281
+ engine.setEffort(level)
281
282
  },
282
283
  setFocusMode: (on: boolean) => setFocusMode(on),
283
284
  setGoal: (text: string) => setGoalText(text),