@miphamai/cli 0.85.5 → 0.85.7

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.
@@ -38,6 +38,10 @@ export class OpenAICompatProvider implements ProviderInstance {
38
38
  Authorization: `Bearer ${apiKey}`,
39
39
  },
40
40
  body: JSON.stringify(body),
41
+ // The caller's cancellation (e.g. `self-critique`'s 2s budget) has to reach
42
+ // the transport, or it is a no-op: `fetch-utils` only combines a caller
43
+ // signal when this field exists.
44
+ signal: req.signal,
41
45
  })
42
46
 
43
47
  if (!response.ok) {
@@ -63,136 +67,147 @@ export class OpenAICompatProvider implements ProviderInstance {
63
67
  // (DeepSeek V4 / reasoning models) aren't mistaken for a stalled connection.
64
68
  const STREAM_READ_TIMEOUT_MS = streamIdleTimeoutMs(req.effort)
65
69
 
66
- while (true) {
67
- let readResult: Awaited<ReturnType<typeof reader.read>>
68
- let idleTimer: ReturnType<typeof setTimeout> | undefined
69
- try {
70
- readResult = await Promise.race([
71
- reader.read(),
72
- new Promise<never>((_, reject) => {
73
- idleTimer = setTimeout(
74
- () =>
75
- reject(
76
- new Error(
77
- `Stream read timeout — no data for ${Math.round(STREAM_READ_TIMEOUT_MS / 1000)}s`,
70
+ // The read loop and the fallback stop share one reader, and that reader owns
71
+ // the connection. `engine.ts` breaks out of this generator on the ordinary
72
+ // `stop` chunk, and a sub-agent throws mid-stream on abort — both call
73
+ // `.return()`, which unwinds through here. Without this, a turn that ends
74
+ // normally (or is abandoned) leaves the body unread and uncancelled, so the
75
+ // socket can't be reused. `cancel()` on an already-errored stream rejects, and
76
+ // on a closed one is a no-op — the catch covers the first.
77
+ try {
78
+ while (true) {
79
+ let readResult: Awaited<ReturnType<typeof reader.read>>
80
+ let idleTimer: ReturnType<typeof setTimeout> | undefined
81
+ try {
82
+ readResult = await Promise.race([
83
+ reader.read(),
84
+ new Promise<never>((_, reject) => {
85
+ idleTimer = setTimeout(
86
+ () =>
87
+ reject(
88
+ new Error(
89
+ `Stream read timeout — no data for ${Math.round(STREAM_READ_TIMEOUT_MS / 1000)}s`,
90
+ ),
78
91
  ),
79
- ),
80
- STREAM_READ_TIMEOUT_MS,
81
- )
82
- }),
83
- ])
84
- } catch (err) {
85
- yield { type: 'error', error: `Stream stalled: ${String(err)}` }
86
- return
87
- } finally {
88
- if (idleTimer) clearTimeout(idleTimer)
89
- }
90
- const { done, value } = readResult
91
- if (done) break
92
-
93
- buffer += decoder.decode(value, { stream: true })
94
- const lines = buffer.split('\n')
95
- buffer = lines.pop() || ''
96
-
97
- for (const line of lines) {
98
- const trimmed = line.trim()
99
- if (!trimmed || !trimmed.startsWith('data: ')) continue
100
- const data = trimmed.slice(6)
101
- if (data === '[DONE]') {
102
- // Emit any pending tool calls before stopping
103
- for (const [, tc] of pendingToolCalls) {
104
- if (!tc.name) continue // drop malformed tool call (missing name)
105
- yield {
106
- type: 'tool_use',
107
- toolUse: {
92
+ STREAM_READ_TIMEOUT_MS,
93
+ )
94
+ }),
95
+ ])
96
+ } catch (err) {
97
+ yield { type: 'error', error: `Stream stalled: ${String(err)}` }
98
+ return
99
+ } finally {
100
+ if (idleTimer) clearTimeout(idleTimer)
101
+ }
102
+ const { done, value } = readResult
103
+ if (done) break
104
+
105
+ buffer += decoder.decode(value, { stream: true })
106
+ const lines = buffer.split('\n')
107
+ buffer = lines.pop() || ''
108
+
109
+ for (const line of lines) {
110
+ const trimmed = line.trim()
111
+ if (!trimmed || !trimmed.startsWith('data: ')) continue
112
+ const data = trimmed.slice(6)
113
+ if (data === '[DONE]') {
114
+ // Emit any pending tool calls before stopping
115
+ for (const [, tc] of pendingToolCalls) {
116
+ if (!tc.name) continue // drop malformed tool call (missing name)
117
+ yield {
108
118
  type: 'tool_use',
109
- id: tc.id || `call_${Date.now()}`,
110
- name: tc.name,
111
- input: this.safeParseJson(tc.arguments),
112
- },
119
+ toolUse: {
120
+ type: 'tool_use',
121
+ id: tc.id || `call_${Date.now()}`,
122
+ name: tc.name,
123
+ input: this.safeParseJson(tc.arguments),
124
+ },
125
+ }
113
126
  }
127
+ yield { type: 'stop', reasoning_content: reasoningContent }
128
+ return
114
129
  }
115
- yield { type: 'stop', reasoning_content: reasoningContent }
116
- return
117
- }
118
130
 
119
- try {
120
- const parsed = JSON.parse(data)
121
- const choice = parsed.choices?.[0]
122
-
123
- // Capture token usage when available (final chunk with stream_options.include_usage)
124
- if (parsed.usage) {
125
- yield {
126
- type: 'usage',
127
- inputTokens: parsed.usage.prompt_tokens,
128
- outputTokens: parsed.usage.completion_tokens,
131
+ try {
132
+ const parsed = JSON.parse(data)
133
+ const choice = parsed.choices?.[0]
134
+
135
+ // Capture token usage when available (final chunk with stream_options.include_usage)
136
+ if (parsed.usage) {
137
+ yield {
138
+ type: 'usage',
139
+ inputTokens: parsed.usage.prompt_tokens,
140
+ outputTokens: parsed.usage.completion_tokens,
141
+ }
129
142
  }
130
- }
131
143
 
132
- if (!choice) continue
144
+ if (!choice) continue
133
145
 
134
- const delta = choice.delta
146
+ const delta = choice.delta
135
147
 
136
- if (delta?.tool_calls) {
137
- for (const tc of delta.tool_calls) {
138
- const idx = tc.index ?? 0
139
- const pending = pendingToolCalls.get(idx) || {
140
- id: '',
141
- name: '',
142
- arguments: '',
143
- }
148
+ if (delta?.tool_calls) {
149
+ for (const tc of delta.tool_calls) {
150
+ const idx = tc.index ?? 0
151
+ const pending = pendingToolCalls.get(idx) || {
152
+ id: '',
153
+ name: '',
154
+ arguments: '',
155
+ }
144
156
 
145
- if (tc.id) pending.id = tc.id
146
- if (tc.function?.name) pending.name = tc.function.name
147
- if (tc.function?.arguments) pending.arguments += tc.function.arguments
157
+ if (tc.id) pending.id = tc.id
158
+ if (tc.function?.name) pending.name = tc.function.name
159
+ if (tc.function?.arguments) pending.arguments += tc.function.arguments
148
160
 
149
- pendingToolCalls.set(idx, pending)
161
+ pendingToolCalls.set(idx, pending)
162
+ }
150
163
  }
151
- }
152
164
 
153
- if (delta?.content) {
154
- yield { type: 'text', content: delta.content }
155
- }
165
+ if (delta?.content) {
166
+ yield { type: 'text', content: delta.content }
167
+ }
156
168
 
157
- if (delta?.reasoning_content) {
158
- reasoningContent += delta.reasoning_content
159
- }
169
+ if (delta?.reasoning_content) {
170
+ reasoningContent += delta.reasoning_content
171
+ }
160
172
 
161
- if (choice.finish_reason === 'tool_calls') {
162
- // Emit fully accumulated tool calls
163
- for (const [, tc] of pendingToolCalls) {
164
- if (!tc.name) continue // drop malformed tool call (missing name)
165
- yield {
166
- type: 'tool_use',
167
- toolUse: {
173
+ if (choice.finish_reason === 'tool_calls') {
174
+ // Emit fully accumulated tool calls
175
+ for (const [, tc] of pendingToolCalls) {
176
+ if (!tc.name) continue // drop malformed tool call (missing name)
177
+ yield {
168
178
  type: 'tool_use',
169
- id: tc.id || `call_${Date.now()}`,
170
- name: tc.name,
171
- input: this.safeParseJson(tc.arguments),
172
- },
179
+ toolUse: {
180
+ type: 'tool_use',
181
+ id: tc.id || `call_${Date.now()}`,
182
+ name: tc.name,
183
+ input: this.safeParseJson(tc.arguments),
184
+ },
185
+ }
173
186
  }
187
+ pendingToolCalls.clear()
174
188
  }
175
- pendingToolCalls.clear()
176
- }
177
189
 
178
- if (choice.finish_reason === 'stop') {
179
- yield { type: 'stop', reasoning_content: reasoningContent }
180
- }
190
+ if (choice.finish_reason === 'stop') {
191
+ yield { type: 'stop', reasoning_content: reasoningContent }
192
+ }
181
193
 
182
- if (choice.finish_reason === 'length') {
183
- // Truncated: the accumulated tool calls were cut off mid-arguments, so
184
- // their JSON is incomplete. Drop them rather than dispatching a broken
185
- // call, and clear the map so the `[DONE]` handler can't emit them either.
186
- pendingToolCalls.clear()
187
- yield { type: 'stop', reasoning_content: reasoningContent, truncated: true }
194
+ if (choice.finish_reason === 'length') {
195
+ // Truncated: the accumulated tool calls were cut off mid-arguments, so
196
+ // their JSON is incomplete. Drop them rather than dispatching a broken
197
+ // call, and clear the map so the `[DONE]` handler can't emit them either.
198
+ pendingToolCalls.clear()
199
+ yield { type: 'stop', reasoning_content: reasoningContent, truncated: true }
200
+ }
201
+ } catch {
202
+ // skip unparseable chunks
188
203
  }
189
- } catch {
190
- // skip unparseable chunks
191
204
  }
192
205
  }
193
- }
194
206
 
195
- yield { type: 'stop', reasoning_content: reasoningContent }
207
+ yield { type: 'stop', reasoning_content: reasoningContent }
208
+ } finally {
209
+ await reader.cancel().catch(() => {})
210
+ }
196
211
  }
197
212
 
198
213
  async listModels(): Promise<ModelInfo[]> {
@@ -47,6 +47,8 @@ const KNOWN_FLAGS = [
47
47
  '--safe-mode',
48
48
  '--resume',
49
49
  '--permission',
50
+ '--provider',
51
+ '--model',
50
52
  ]
51
53
 
52
54
  /**
@@ -60,8 +62,16 @@ const KNOWN_FLAGS = [
60
62
  * `--permission` is here for the same reason and for one more: `mipham attach <id>
61
63
  * --permission plan` reads the session id through `firstPositional`, and without this
62
64
  * entry it would have taken `plan` for a session id.
65
+ *
66
+ * `--provider`/`--model` are the same defect a third time, and here it was *measured*
67
+ * rather than reasoned about: both shipped IDE integrations build
68
+ * `mipham --provider <id> --model <id>` from their settings (`infrastructure/vscode/
69
+ * extension.js`, `MiphamAction.kt`), and that command was answered with
70
+ * `Unknown command: mipham deepseek` — the provider *value* blamed, the flag that was
71
+ * actually wrong never mentioned, exit 1, no CLI. Their values are open (a provider may
72
+ * be user-defined), so — unlike `--permission` — this module owns only their spelling.
63
73
  */
64
- const VALUE_FLAGS = ['--resume', '--permission']
74
+ const VALUE_FLAGS = ['--resume', '--permission', '--provider', '--model']
65
75
 
66
76
  /**
67
77
  * The first token that would be read as a command, skipping flags and the values
@@ -9,7 +9,7 @@
9
9
  export const PACKAGE_NAME = '@miphamai/cli' as const
10
10
 
11
11
  /** 当前发布版本 */
12
- export const PACKAGE_VERSION = '0.85.5' as const
12
+ export const PACKAGE_VERSION = '0.85.7' as const
13
13
 
14
14
  /** npm install 全局安装命令 */
15
15
  export const NPM_INSTALL_COMMAND = `npm install -g ${PACKAGE_NAME}` as const
@@ -56,3 +56,38 @@ export const COMPANY_NAME_ZH = '北京华安麦逄科技有限公司' as const
56
56
 
57
57
  /** 公司简称 */
58
58
  export const COMPANY_SHORT = '华安麦逄科技' as const
59
+
60
+ /**
61
+ * 计数类常量 —— 公开面(两个官网的产品页)与文档消费的那几个数。
62
+ *
63
+ * **不要手改。** 这四个数由 `apps/cli/scripts/sync-counts.ts` 从真源产出后回写:
64
+ * 命令 / 提供商 / 工具在**进程内算出**(`getCommandNames()` / `DEFAULT_PROVIDERS` /
65
+ * `createToolRegistry()`),测试数由**一次真套件跑**的自报总数产出。
66
+ *
67
+ * 为什么要有这几个槽位(2026-09-25):两个官网的产品页把「137 命令 · 3473 测试」
68
+ * 当**字面量**写死,没有真源 ⇒ 只能靠人记得去改,同一处**至少手改过 7 次**
69
+ * (2262 → … → 3473),而每次手改本身还会再漂。站点侧的传播链其实一直存在 ——
70
+ * 两站的 deploy 脚本都 `cp` 本文件覆盖自己那份 `src/config/package-info.json`
71
+ * —— 缺的只是**槽位**。名字/版本有槽位所以不漂,计数连槽位都没有。
72
+ *
73
+ * 守卫:`apps/cli/test/integrity/published-counts.test.ts`(三个进程内计数与落盘值
74
+ * 逐一对齐);测试总数另由 CI 的 Test job 与套件自报的总数比对(硬门禁)。
75
+ */
76
+
77
+ /** Slash 命令总数(真源:`getCommandNames().length`,`apps/cli/src/ui/commands.ts`) */
78
+ export const SLASH_COMMAND_COUNT = 137 as const
79
+
80
+ /** 内置提供商总数(真源:`DEFAULT_PROVIDERS.length`,`apps/cli/src/shared/constants.ts`) */
81
+ export const PROVIDER_COUNT = 12 as const
82
+
83
+ /** 已注册工具总数(真源:`createToolRegistry().size`,`apps/cli/src/tools/index.ts`) */
84
+ export const TOOL_COUNT = 31 as const
85
+
86
+ /**
87
+ * 测试总数(真源:**一次真套件跑**的自报总数)。
88
+ *
89
+ * 取**总数**而不是 `passed`:本机(macOS)与 CI(Linux)的 passed/skipped 切分**不同**
90
+ * —— `test/e2e/full-pipeline.test.ts` 在 Linux 上整文件 skip、在 macOS 上跑 ——
91
+ * 但**总数相同**(两边都把被 skip 的算进去)。
92
+ */
93
+ export const TEST_COUNT = 3535 as const