@miphamai/cli 0.6.2 → 0.6.4

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.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -140,6 +140,7 @@ export class QueryEngine {
140
140
  const toolDefs = this.getToolDefinitions()
141
141
 
142
142
  let assistantContent = ''
143
+ let reasoningContent = ''
143
144
  const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
144
145
 
145
146
  // Stream model response
@@ -162,6 +163,10 @@ export class QueryEngine {
162
163
  assistantContent += chunk.content
163
164
  }
164
165
 
166
+ if (chunk.reasoning_content) {
167
+ reasoningContent += chunk.reasoning_content
168
+ }
169
+
165
170
  if (chunk.type === 'tool_use' && chunk.toolUse) {
166
171
  toolUses.push({
167
172
  id: chunk.toolUse.id,
@@ -172,16 +177,26 @@ export class QueryEngine {
172
177
 
173
178
  if (chunk.type === 'stop') {
174
179
  // Add assistant response to context
175
- if (assistantContent) {
176
- this.context.addMessage({ role: 'assistant', content: assistantContent })
180
+ if (assistantContent || reasoningContent) {
181
+ const msg: import('../shared/types').Message = {
182
+ role: 'assistant',
183
+ content: assistantContent || '',
184
+ }
185
+ if (reasoningContent) msg.reasoning_content = reasoningContent
186
+ this.context.addMessage(msg)
177
187
  }
178
188
  }
179
189
  }
180
190
  } catch (err) {
181
191
  if (isAbortError(err)) {
182
192
  // User interrupted — keep partial content, stop gracefully
183
- if (assistantContent) {
184
- this.context.addMessage({ role: 'assistant', content: assistantContent })
193
+ if (assistantContent || reasoningContent) {
194
+ const msg: import('../shared/types').Message = {
195
+ role: 'assistant',
196
+ content: assistantContent || '',
197
+ }
198
+ if (reasoningContent) msg.reasoning_content = reasoningContent
199
+ this.context.addMessage(msg)
185
200
  }
186
201
  yield { type: 'stop' }
187
202
  return
@@ -259,6 +274,7 @@ export class QueryEngine {
259
274
  const messages = this.context.getMessages()
260
275
 
261
276
  let assistantContent = ''
277
+ let reasoningContent = ''
262
278
  const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
263
279
 
264
280
  try {
@@ -274,6 +290,10 @@ export class QueryEngine {
274
290
  if (chunk.type === 'error') return
275
291
  if (chunk.type === 'text' && chunk.content) assistantContent += chunk.content
276
292
 
293
+ if (chunk.reasoning_content) {
294
+ reasoningContent += chunk.reasoning_content
295
+ }
296
+
277
297
  if (chunk.type === 'tool_use' && chunk.toolUse) {
278
298
  toolUses.push({
279
299
  id: chunk.toolUse.id,
@@ -284,8 +304,13 @@ export class QueryEngine {
284
304
  }
285
305
  } catch (err) {
286
306
  if (isAbortError(err)) {
287
- if (assistantContent) {
288
- this.context.addMessage({ role: 'assistant', content: assistantContent })
307
+ if (assistantContent || reasoningContent) {
308
+ const msg: import('../shared/types').Message = {
309
+ role: 'assistant',
310
+ content: assistantContent || '',
311
+ }
312
+ if (reasoningContent) msg.reasoning_content = reasoningContent
313
+ this.context.addMessage(msg)
289
314
  }
290
315
  yield { type: 'stop' }
291
316
  return
@@ -294,8 +319,13 @@ export class QueryEngine {
294
319
  return
295
320
  }
296
321
 
297
- if (assistantContent) {
298
- this.context.addMessage({ role: 'assistant', content: assistantContent })
322
+ if (assistantContent || reasoningContent) {
323
+ const msg: import('../shared/types').Message = {
324
+ role: 'assistant',
325
+ content: assistantContent || '',
326
+ }
327
+ if (reasoningContent) msg.reasoning_content = reasoningContent
328
+ this.context.addMessage(msg)
299
329
  }
300
330
 
301
331
  // No more tool calls — fire Stop hook and potentially continue
@@ -401,6 +431,27 @@ export class QueryEngine {
401
431
  return this.tools
402
432
  }
403
433
 
434
+ /** Register a tool dynamically (used by MCP auto-registration). */
435
+ registerTool(tool: ToolDefinition): void {
436
+ if (this.tools.has(tool.name)) {
437
+ process.stderr.write(`[mcp] Tool collision: "${tool.name}" already registered. Skipping.\n`)
438
+ return
439
+ }
440
+ this.tools.set(tool.name, tool)
441
+ }
442
+
443
+ /** Remove a dynamically registered tool by name. */
444
+ unregisterTool(name: string): void {
445
+ this.tools.delete(name)
446
+ }
447
+
448
+ /** Register multiple tools at once. */
449
+ registerTools(tools: ToolDefinition[]): void {
450
+ for (const tool of tools) {
451
+ this.tools.set(tool.name, tool)
452
+ }
453
+ }
454
+
404
455
  switchProvider(providerId: string, modelId?: string): void {
405
456
  this.registry.switchProvider(providerId, modelId)
406
457
  }
package/src/index.tsx CHANGED
@@ -11,6 +11,7 @@ import { SkillsLoader } from './skills/loader'
11
11
  import { PluginManager } from './plugin/plugin-manager'
12
12
  import { createToolRegistry } from './tools'
13
13
  import { McpClient } from './mcp/client'
14
+ import { registerMcpServerTools } from './mcp/registry'
14
15
  import { HookEngine } from './core/hooks'
15
16
  import { ArtifactServer } from './artifacts/server'
16
17
  import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
@@ -50,17 +51,6 @@ export async function runApp(options: RunOptions): Promise<void> {
50
51
  // Load configuration
51
52
  const config = loadConfig()
52
53
 
53
- // Connect MCP servers (fire-and-forget — do not block startup)
54
- const mcpServers = config.skills?.mcpServers ?? []
55
- if (mcpServers.length > 0) {
56
- const mcp = McpClient.getInstance()
57
- for (const server of mcpServers) {
58
- mcp.connect(server).catch((err) => {
59
- process.stderr.write(`[mcp] Failed to connect "${server.name}": ${String(err)}\n`)
60
- })
61
- }
62
- }
63
-
64
54
  // Bootstrap providers
65
55
  const defaultProvider = options.provider || config.defaultProvider
66
56
  const defaultModel = options.model || config.defaultModel
@@ -97,9 +87,34 @@ export async function runApp(options: RunOptions): Promise<void> {
97
87
  context.setSystemPrompt(instructions.buildSystemPrompt())
98
88
  }
99
89
 
100
- // Create tool registry with all 16 tools
90
+ // Create tool registry with all built-in tools
101
91
  const tools = createToolRegistry()
102
92
 
93
+ // Connect MCP servers and register their tools into the tool registry.
94
+ // Uses Promise.allSettled for parallel connection — failures are non-fatal.
95
+ const mcpServers = config.skills?.mcpServers ?? []
96
+ if (mcpServers.length > 0) {
97
+ const mcp = McpClient.getInstance()
98
+ const results = await Promise.allSettled(
99
+ mcpServers.map(async (server) => {
100
+ await mcp.connect(server)
101
+ const count = registerMcpServerTools(server.name, tools)
102
+ if (count > 0) {
103
+ process.stderr.write(`[mcp] "${server.name}": registered ${count} tools\n`)
104
+ }
105
+ }),
106
+ )
107
+ // Log failures (non-fatal — app starts without that server's tools)
108
+ for (let i = 0; i < results.length; i++) {
109
+ const result = results[i]!
110
+ if (result.status === 'rejected') {
111
+ process.stderr.write(
112
+ `[mcp] Failed to connect "${mcpServers[i]!.name}": ${String(result.reason)}\n`,
113
+ )
114
+ }
115
+ }
116
+ }
117
+
103
118
  // Initialize hook engine — register skill-defined hooks
104
119
  const hookEngine = new HookEngine()
105
120
  for (const skill of skillsLoader.list()) {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * MCP Tool Registry — converts MCP server tools into main ToolDefinitions
3
+ * and registers them into the central tool registry.
4
+ *
5
+ * Naming convention: mcp__<sanitizedServerName>__<sanitizedToolName>
6
+ * Example: server "chrome-devtools-mcp" tool "click" → "mcp__chrome_devtools_mcp__click"
7
+ */
8
+
9
+ import type { ToolDefinition as McpToolDefinition, ToolCallResult } from './types'
10
+ import type { ToolDefinition, ToolResult, ToolContext } from '../shared/types'
11
+ import { McpClient } from './client'
12
+
13
+ const MCP_TOOL_PREFIX = 'mcp__'
14
+ const MAX_NAME_LENGTH = 128
15
+
16
+ /**
17
+ * Sanitize a name for use in a tool identifier.
18
+ * Lowercases and replaces non-alphanumeric characters (except hyphens) with underscores.
19
+ */
20
+ function sanitizeName(name: string): string {
21
+ return name
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9-]/g, '_')
24
+ .replace(/_+/g, '_')
25
+ .replace(/^_|_$/g, '')
26
+ }
27
+
28
+ /**
29
+ * Build a namespaced MCP tool name.
30
+ * Format: mcp__<sanitizedServerName>__<sanitizedToolName>
31
+ */
32
+ function buildToolName(serverName: string, toolName: string): string {
33
+ const base = `${MCP_TOOL_PREFIX}${sanitizeName(serverName)}__${sanitizeName(toolName)}`
34
+ if (base.length <= MAX_NAME_LENGTH) return base
35
+ const prefix = `${MCP_TOOL_PREFIX}${sanitizeName(serverName)}__`
36
+ const maxToolLen = MAX_NAME_LENGTH - prefix.length
37
+ return prefix + sanitizeName(toolName).slice(0, maxToolLen)
38
+ }
39
+
40
+ /**
41
+ * Convert an MCP server ToolDefinition into the main ToolDefinition format
42
+ * that can be registered in the central tool registry.
43
+ */
44
+ export function convertMcpTool(serverName: string, mcpTool: McpToolDefinition): ToolDefinition {
45
+ const toolName = buildToolName(serverName, mcpTool.name)
46
+
47
+ // Build parameters schema — pass through MCP inputSchema as-is
48
+ const parameters: Record<string, unknown> = {
49
+ type: 'object',
50
+ properties: (mcpTool.inputSchema.properties as Record<string, unknown>) ?? {},
51
+ }
52
+ if (mcpTool.inputSchema.required) {
53
+ parameters.required = mcpTool.inputSchema.required
54
+ }
55
+
56
+ return {
57
+ name: toolName,
58
+ description: `[MCP:${serverName}] ${mcpTool.description || mcpTool.name}`,
59
+ category: 'system',
60
+ permission: 'ask',
61
+ parameters,
62
+ async execute(params: Record<string, unknown>, _ctx: ToolContext): Promise<ToolResult> {
63
+ const client = McpClient.getInstance()
64
+
65
+ const conn = client.getConnection(serverName)
66
+ if (!conn || conn.status !== 'connected') {
67
+ return {
68
+ success: false,
69
+ content: '',
70
+ error: `MCP server "${serverName}" is not connected (status: ${conn?.status || 'unknown'}). The server may have crashed or been disconnected.`,
71
+ }
72
+ }
73
+
74
+ const result: ToolCallResult = await client.callTool(serverName, mcpTool.name, params)
75
+
76
+ const text = result.content
77
+ .map((c) => {
78
+ if (c.type === 'text' && c.text) return c.text
79
+ if (c.type === 'image') return `[Image: ${c.mimeType || 'unknown'}]`
80
+ return JSON.stringify(c)
81
+ })
82
+ .join('\n')
83
+
84
+ return {
85
+ success: !result.isError,
86
+ content: text || '(empty result)',
87
+ error: result.isError ? text : undefined,
88
+ }
89
+ },
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Register all tools from a connected MCP server into the tool registry.
95
+ * Returns the number of tools registered.
96
+ */
97
+ export function registerMcpServerTools(
98
+ serverName: string,
99
+ toolsMap: Map<string, ToolDefinition>,
100
+ ): number {
101
+ const client = McpClient.getInstance()
102
+ const mcpTools = client.getTools(serverName)
103
+ let registered = 0
104
+
105
+ for (const mcpTool of mcpTools) {
106
+ try {
107
+ const tool = convertMcpTool(serverName, mcpTool)
108
+
109
+ if (toolsMap.has(tool.name)) {
110
+ process.stderr.write(
111
+ `[mcp] Tool collision: "${tool.name}" (from server "${serverName}") conflicts with existing tool. Skipping.\n`,
112
+ )
113
+ continue
114
+ }
115
+
116
+ toolsMap.set(tool.name, tool)
117
+ registered++
118
+ } catch (err) {
119
+ process.stderr.write(
120
+ `[mcp] Failed to register tool "${mcpTool.name}" from server "${serverName}": ${String(err)}\n`,
121
+ )
122
+ }
123
+ }
124
+
125
+ return registered
126
+ }
127
+
128
+ /**
129
+ * Unregister all tools previously registered from a specific MCP server.
130
+ * Tools are identified by the mcp__<serverName>__ prefix.
131
+ */
132
+ export function unregisterMcpServerTools(
133
+ serverName: string,
134
+ toolsMap: Map<string, ToolDefinition>,
135
+ ): void {
136
+ const prefix = `${MCP_TOOL_PREFIX}${sanitizeName(serverName)}__`
137
+ for (const name of toolsMap.keys()) {
138
+ if (name.startsWith(prefix)) {
139
+ toolsMap.delete(name)
140
+ }
141
+ }
142
+ }
@@ -44,8 +44,9 @@ export class OpenAICompatProvider implements ProviderInstance {
44
44
  const decoder = new TextDecoder()
45
45
  let buffer = ''
46
46
 
47
- // Track incremental tool calls across streaming deltas
47
+ // Track incremental tool calls and reasoning content across streaming deltas
48
48
  const pendingToolCalls = new Map<number, { id: string; name: string; arguments: string }>()
49
+ let reasoningContent = ''
49
50
 
50
51
  while (true) {
51
52
  const { done, value } = await reader.read()
@@ -72,7 +73,7 @@ export class OpenAICompatProvider implements ProviderInstance {
72
73
  },
73
74
  }
74
75
  }
75
- yield { type: 'stop' }
76
+ yield { type: 'stop', reasoning_content: reasoningContent }
76
77
  return
77
78
  }
78
79
 
@@ -104,6 +105,10 @@ export class OpenAICompatProvider implements ProviderInstance {
104
105
  yield { type: 'text', content: delta.content }
105
106
  }
106
107
 
108
+ if (delta?.reasoning_content) {
109
+ reasoningContent += delta.reasoning_content
110
+ }
111
+
107
112
  if (choice.finish_reason === 'tool_calls') {
108
113
  // Emit fully accumulated tool calls
109
114
  for (const [, tc] of pendingToolCalls) {
@@ -121,7 +126,7 @@ export class OpenAICompatProvider implements ProviderInstance {
121
126
  }
122
127
 
123
128
  if (choice.finish_reason === 'stop') {
124
- yield { type: 'stop' }
129
+ yield { type: 'stop', reasoning_content: reasoningContent }
125
130
  }
126
131
  } catch {
127
132
  // skip unparseable chunks
@@ -129,7 +134,7 @@ export class OpenAICompatProvider implements ProviderInstance {
129
134
  }
130
135
  }
131
136
 
132
- yield { type: 'stop' }
137
+ yield { type: 'stop', reasoning_content: reasoningContent }
133
138
  }
134
139
 
135
140
  async listModels(): Promise<ModelInfo[]> {
@@ -174,7 +179,7 @@ export class OpenAICompatProvider implements ProviderInstance {
174
179
  next.content.some((b) => b.type === 'tool_use')
175
180
  ) {
176
181
  const toolUses = next.content.filter((b) => b.type === 'tool_use')
177
- result.push({
182
+ const combinedMsg: Record<string, unknown> = {
178
183
  role: 'assistant',
179
184
  content: msg.content,
180
185
  tool_calls: toolUses.map((tu) => ({
@@ -185,12 +190,20 @@ export class OpenAICompatProvider implements ProviderInstance {
185
190
  arguments: JSON.stringify(tu.input),
186
191
  },
187
192
  })),
188
- })
193
+ }
194
+ if (msg.reasoning_content) {
195
+ combinedMsg.reasoning_content = msg.reasoning_content
196
+ }
197
+ result.push(combinedMsg)
189
198
  i++ // skip the next message (consumed)
190
199
  continue
191
200
  }
192
201
 
193
- result.push({ role: msg.role, content: msg.content })
202
+ const standaloneMsg: Record<string, unknown> = { role: msg.role, content: msg.content }
203
+ if (msg.role === 'assistant' && msg.reasoning_content) {
204
+ standaloneMsg.reasoning_content = msg.reasoning_content
205
+ }
206
+ result.push(standaloneMsg)
194
207
  continue
195
208
  }
196
209
 
@@ -207,7 +220,7 @@ export class OpenAICompatProvider implements ProviderInstance {
207
220
  .map((b) => (b.type === 'text' ? b.text : ''))
208
221
  .join('') || null
209
222
 
210
- result.push({
223
+ const toolUseMsg: Record<string, unknown> = {
211
224
  role: 'assistant',
212
225
  content: textContent,
213
226
  tool_calls: toolUses.map((tu) => ({
@@ -218,7 +231,11 @@ export class OpenAICompatProvider implements ProviderInstance {
218
231
  arguments: JSON.stringify(tu.input),
219
232
  },
220
233
  })),
221
- })
234
+ }
235
+ if (msg.reasoning_content) {
236
+ toolUseMsg.reasoning_content = msg.reasoning_content
237
+ }
238
+ result.push(toolUseMsg)
222
239
  continue
223
240
  }
224
241
 
@@ -46,6 +46,8 @@ export type ContentBlock = TextContent | ImageContent | ToolUseContent | ToolRes
46
46
  export interface Message {
47
47
  role: 'system' | 'user' | 'assistant'
48
48
  content: string | ContentBlock[]
49
+ /** DeepSeek reasoning tokens — must be passed back to the API in multi-turn conversations. */
50
+ reasoning_content?: string
49
51
  }
50
52
 
51
53
  // ── Tool Types ──
@@ -105,6 +107,8 @@ export interface StreamChunk {
105
107
  toolUse?: ToolUseContent
106
108
  tool_use_id?: string
107
109
  error?: string
110
+ /** DeepSeek reasoning tokens accumulated during this stream. */
111
+ reasoning_content?: string
108
112
  }
109
113
 
110
114
  // ── Config Types ──