@miphamai/cli 0.6.3 → 0.6.5

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.3",
3
+ "version": "0.6.5",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -8,12 +8,14 @@ import type { AgentRegistry } from '../agent/agent-registry'
8
8
  import { analyzeForMemory } from './memory/memory-writer'
9
9
  import { getMemoryManager } from './memory/memory-loader'
10
10
  import type { AgentViewManager } from '../agent-view/agent-view-manager'
11
+ import type { SkillsLoader } from '../skills/loader'
11
12
 
12
13
  export class QueryEngine {
13
14
  private hookEngine?: HookEngine
14
15
  private artifactServer?: ArtifactServer
15
16
  private agentRegistry?: AgentRegistry
16
17
  private agentViewManager?: AgentViewManager
18
+ private skillsLoader?: SkillsLoader
17
19
  private goal?: string
18
20
  private maxGoalLoops = 20
19
21
  private lastAssistantContent?: string
@@ -55,6 +57,11 @@ export class QueryEngine {
55
57
  return this.agentViewManager
56
58
  }
57
59
 
60
+ /** Register the SkillsLoader for Skill tool context injection. */
61
+ setSkillsLoader(loader: SkillsLoader): void {
62
+ this.skillsLoader = loader
63
+ }
64
+
58
65
  /** Set the session goal for goal-driven execution. */
59
66
  setGoal(goal: string): void {
60
67
  this.goal = goal
@@ -393,6 +400,7 @@ export class QueryEngine {
393
400
  sessionId: 'session-1',
394
401
  provider: this.registry.getActive().config.id,
395
402
  model: this.registry.getActiveModel(),
403
+ skillsLoader: this.skillsLoader,
396
404
  registry: this.registry,
397
405
  toolRegistry: this.tools,
398
406
  artifactServer: this.artifactServer,
@@ -431,6 +439,27 @@ export class QueryEngine {
431
439
  return this.tools
432
440
  }
433
441
 
442
+ /** Register a tool dynamically (used by MCP auto-registration). */
443
+ registerTool(tool: ToolDefinition): void {
444
+ if (this.tools.has(tool.name)) {
445
+ process.stderr.write(`[mcp] Tool collision: "${tool.name}" already registered. Skipping.\n`)
446
+ return
447
+ }
448
+ this.tools.set(tool.name, tool)
449
+ }
450
+
451
+ /** Remove a dynamically registered tool by name. */
452
+ unregisterTool(name: string): void {
453
+ this.tools.delete(name)
454
+ }
455
+
456
+ /** Register multiple tools at once. */
457
+ registerTools(tools: ToolDefinition[]): void {
458
+ for (const tool of tools) {
459
+ this.tools.set(tool.name, tool)
460
+ }
461
+ }
462
+
434
463
  switchProvider(providerId: string, modelId?: string): void {
435
464
  this.registry.switchProvider(providerId, modelId)
436
465
  }
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
@@ -73,10 +63,17 @@ export async function runApp(options: RunOptions): Promise<void> {
73
63
  // Load skills
74
64
  const skillsLoader = new SkillsLoader()
75
65
  skillsLoader.loadBuiltin(process.cwd())
66
+ skillsLoader.loadUserSkills()
76
67
  if (config.skills?.paths) {
77
68
  skillsLoader.loadExternal(config.skills.paths)
78
69
  }
79
70
 
71
+ // Inject skills system-reminder into system prompt for AI auto-triggering
72
+ const skillsReminder = skillsLoader.buildSystemReminder()
73
+ if (skillsReminder) {
74
+ instructions.setSkillsReminder(skillsReminder)
75
+ }
76
+
80
77
  // Initialize plugin manager
81
78
  const pluginManager = new PluginManager()
82
79
 
@@ -97,9 +94,34 @@ export async function runApp(options: RunOptions): Promise<void> {
97
94
  context.setSystemPrompt(instructions.buildSystemPrompt())
98
95
  }
99
96
 
100
- // Create tool registry with all 16 tools
97
+ // Create tool registry with all built-in tools
101
98
  const tools = createToolRegistry()
102
99
 
100
+ // Connect MCP servers and register their tools into the tool registry.
101
+ // Uses Promise.allSettled for parallel connection — failures are non-fatal.
102
+ const mcpServers = config.skills?.mcpServers ?? []
103
+ if (mcpServers.length > 0) {
104
+ const mcp = McpClient.getInstance()
105
+ const results = await Promise.allSettled(
106
+ mcpServers.map(async (server) => {
107
+ await mcp.connect(server)
108
+ const count = registerMcpServerTools(server.name, tools)
109
+ if (count > 0) {
110
+ process.stderr.write(`[mcp] "${server.name}": registered ${count} tools\n`)
111
+ }
112
+ }),
113
+ )
114
+ // Log failures (non-fatal — app starts without that server's tools)
115
+ for (let i = 0; i < results.length; i++) {
116
+ const result = results[i]!
117
+ if (result.status === 'rejected') {
118
+ process.stderr.write(
119
+ `[mcp] Failed to connect "${mcpServers[i]!.name}": ${String(result.reason)}\n`,
120
+ )
121
+ }
122
+ }
123
+ }
124
+
103
125
  // Initialize hook engine — register skill-defined hooks
104
126
  const hookEngine = new HookEngine()
105
127
  for (const skill of skillsLoader.list()) {
@@ -119,6 +141,7 @@ export async function runApp(options: RunOptions): Promise<void> {
119
141
  engine.setHookEngine(hookEngine)
120
142
  engine.setArtifactServer(artifactServer)
121
143
  engine.setAgentViewManager(agentViewManager)
144
+ engine.setSkillsLoader(skillsLoader)
122
145
  engine.setupContextSummarizer()
123
146
 
124
147
  // Auto-save session on exit
@@ -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
+ }
@@ -147,6 +147,8 @@ export interface SkillDefinition {
147
147
  disableModelInvocation?: boolean
148
148
  /** When true, users can invoke this skill directly via /<name> */
149
149
  userInvocable?: boolean
150
+ /** The markdown body content of the skill file (instructions for the AI to follow). */
151
+ body?: string
150
152
  }
151
153
 
152
154
  // ── Hook Types ──
@@ -98,13 +98,14 @@ export class SkillsLoader {
98
98
  private tryLoad(path: string, type: 'standard' | 'mipham'): void {
99
99
  try {
100
100
  const raw = readFileSync(path, 'utf-8')
101
- const { data } = parseFrontmatter(raw)
101
+ const { data, content } = parseFrontmatter(raw)
102
102
 
103
103
  const skill: SkillDefinition = {
104
104
  name: (data.name as string) || this.nameFromPath(path),
105
105
  description: (data.description as string) || '',
106
106
  version: (data.version as string) || '0.1.0',
107
107
  type,
108
+ body: content.trim() || undefined,
108
109
  tools: data.tools as SkillDefinition['tools'],
109
110
  hooks: data.hooks as SkillDefinition['hooks'],
110
111
  prompts: data.prompts as SkillDefinition['prompts'],
@@ -64,15 +64,16 @@ export const skillTool: ToolDefinition = {
64
64
  }
65
65
  }
66
66
 
67
- // Standard inline execution — return metadata for AI to follow
67
+ // Standard inline execution — return skill body for AI to follow
68
68
  const lines: string[] = [
69
69
  `── Skill Invoked: ${skill.name} ──`,
70
70
  `Type: ${skill.type} | Version: ${skill.version}`,
71
71
  skill.description ? `Description: ${skill.description}` : '',
72
72
  args ? `Arguments: ${args}` : '',
73
73
  '',
74
- 'The AI should now follow the instructions from this skill.',
75
- 'Skill content has been loaded into context.',
74
+ 'The AI should now follow these instructions:',
75
+ '',
76
+ skill.body || '(no instructions body)',
76
77
  ].filter(Boolean)
77
78
 
78
79
  return { success: true, content: lines.join('\n') }