@miphamai/cli 0.6.5 → 0.6.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miphamai/cli",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
5
5
  "keywords": [
6
6
  "ai",
@@ -75,24 +75,97 @@ export class SubAgent {
75
75
  const resolvedModel = modelToUse === 'inherit' ? model : modelToUse
76
76
 
77
77
  const chunks: string[] = []
78
+ const MAX_TOOL_TURNS = options.maxTurns || 5
79
+
80
+ let currentMessages = messages
81
+ let currentSystemPrompt = systemPrompt
78
82
 
79
83
  try {
80
- for await (const chunk of provider.chat({
81
- model: resolvedModel,
82
- messages,
83
- systemPrompt,
84
- tools: toolDefs,
85
- maxTokens: 4096,
86
- })) {
87
- if (chunk.type === 'text' && chunk.content) {
88
- chunks.push(chunk.content)
89
- }
90
- if (chunk.type === 'error') {
91
- throw new Error(`Sub-agent execution failed: ${chunk.error}`)
84
+ for (let turn = 0; turn < MAX_TOOL_TURNS; turn++) {
85
+ const toolUses: Array<{ id: string; name: string; input: Record<string, unknown> }> = []
86
+ let turnText = ''
87
+
88
+ for await (const chunk of provider.chat({
89
+ model: resolvedModel,
90
+ messages: currentMessages,
91
+ systemPrompt: currentSystemPrompt,
92
+ tools: toolDefs,
93
+ maxTokens: 4096,
94
+ })) {
95
+ if (chunk.type === 'text' && chunk.content) {
96
+ turnText += chunk.content
97
+ }
98
+ if (chunk.type === 'tool_use' && chunk.toolUse) {
99
+ toolUses.push({
100
+ id: chunk.toolUse.id,
101
+ name: chunk.toolUse.name,
102
+ input: chunk.toolUse.input,
103
+ })
104
+ }
105
+ if (chunk.type === 'error') {
106
+ throw new Error(`Sub-agent execution failed: ${chunk.error}`)
107
+ }
108
+ if (chunk.type === 'stop') {
109
+ break
110
+ }
92
111
  }
93
- if (chunk.type === 'stop') {
94
- break
112
+
113
+ chunks.push(turnText)
114
+
115
+ // No tools used — we're done
116
+ if (toolUses.length === 0) break
117
+
118
+ // Execute tools and build follow-up messages
119
+ currentMessages = [
120
+ ...currentMessages,
121
+ {
122
+ role: 'assistant' as const,
123
+ content: turnText || 'Executing tools...',
124
+ },
125
+ ]
126
+
127
+ for (const tu of toolUses) {
128
+ const tool = this.toolRegistry.get(tu.name)
129
+ if (!tool) {
130
+ currentMessages.push({
131
+ role: 'user' as const,
132
+ content: `Error: Unknown tool "${tu.name}"`,
133
+ })
134
+ continue
135
+ }
136
+
137
+ try {
138
+ const result = await tool.execute(tu.input, {
139
+ cwd: process.cwd(),
140
+ sessionId: 'sub-agent',
141
+ provider: '',
142
+ model: resolvedModel,
143
+ })
144
+
145
+ currentMessages.push({
146
+ role: 'assistant' as const,
147
+ content: [{ type: 'tool_use' as const, id: tu.id, name: tu.name, input: tu.input }],
148
+ })
149
+ currentMessages.push({
150
+ role: 'user' as const,
151
+ content: [
152
+ {
153
+ type: 'tool_result' as const,
154
+ tool_use_id: tu.id,
155
+ content: result.success ? result.content : result.error || result.content,
156
+ },
157
+ ],
158
+ })
159
+ } catch (err) {
160
+ currentMessages.push({
161
+ role: 'user' as const,
162
+ content: `Tool "${tu.name}" execution error: ${String(err)}`,
163
+ })
164
+ }
95
165
  }
166
+
167
+ // Don't send system prompt on subsequent turns
168
+ currentSystemPrompt = ''
96
169
  }
97
170
  } catch (err) {
98
171
  if (err instanceof Error && err.message.startsWith('Sub-agent')) {
@@ -36,4 +36,6 @@ export interface SubAgentOptions {
36
36
  maxContextMessages?: number
37
37
  allowedTools?: string[]
38
38
  modelOverride?: string
39
+ /** Maximum tool-calling turns (default: 5) to prevent infinite loops. */
40
+ maxTurns?: number
39
41
  }
package/src/index.tsx CHANGED
@@ -9,9 +9,11 @@ import { QueryEngine } from './core/engine'
9
9
  import { SessionStore } from './core/session-store'
10
10
  import { SkillsLoader } from './skills/loader'
11
11
  import { PluginManager } from './plugin/plugin-manager'
12
+ import { loadPlugins } from './plugin/plugin-loader'
12
13
  import { createToolRegistry } from './tools'
13
14
  import { McpClient } from './mcp/client'
14
15
  import { registerMcpServerTools } from './mcp/registry'
16
+ import { AgentRegistry } from './agent/agent-registry'
15
17
  import { HookEngine } from './core/hooks'
16
18
  import { ArtifactServer } from './artifacts/server'
17
19
  import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
@@ -142,6 +144,22 @@ export async function runApp(options: RunOptions): Promise<void> {
142
144
  engine.setArtifactServer(artifactServer)
143
145
  engine.setAgentViewManager(agentViewManager)
144
146
  engine.setSkillsLoader(skillsLoader)
147
+
148
+ // Initialize agent registry and load plugin agents/skills/MCP/hooks
149
+ const agentRegistry = new AgentRegistry()
150
+ agentRegistry.loadUserAgents()
151
+ agentRegistry.loadProjectAgents(process.cwd())
152
+ engine.setAgentRegistry(agentRegistry)
153
+
154
+ loadPlugins(
155
+ pluginManager,
156
+ agentRegistry,
157
+ skillsLoader,
158
+ hookEngine,
159
+ McpClient.getInstance(),
160
+ tools,
161
+ )
162
+
145
163
  engine.setupContextSummarizer()
146
164
 
147
165
  // Auto-save session on exit
@@ -1,36 +1,116 @@
1
- import type { PluginManager } from './plugin-manager'
2
1
  import { join } from 'node:path'
3
- import { existsSync } from 'node:fs'
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs'
3
+ import type { PluginManager } from './plugin-manager'
4
+ import type { AgentRegistry } from '../agent/agent-registry'
5
+ import type { SkillsLoader } from '../skills/loader'
6
+ import type { HookEngine } from '../core/hooks'
7
+ import type { McpClient } from '../mcp/client'
8
+ import { registerMcpServerTools } from '../mcp/registry'
9
+ import { executeHook } from '../core/hooks-executor'
10
+ import type { McpServerConfig, ToolDefinition, HookConfig, HookEvent } from '../shared/types'
4
11
 
12
+ /**
13
+ * Activate all enabled plugins — load their agents, skills, MCP servers, and hooks.
14
+ *
15
+ * Called once at startup after the core subsystems (agent registry, skills loader,
16
+ * hook engine, MCP client, tool registry) are initialized.
17
+ */
5
18
  export function loadPlugins(
6
19
  pluginManager: PluginManager,
7
- engine: unknown,
8
- skillsLoader: unknown,
20
+ agentRegistry: AgentRegistry,
21
+ skillsLoader: SkillsLoader,
22
+ hookEngine: HookEngine,
23
+ mcpClient: McpClient,
24
+ toolsMap: Map<string, ToolDefinition>,
9
25
  ): void {
10
26
  for (const plugin of pluginManager.getEnabled()) {
11
- // Load custom agents
27
+ // ── Custom agents ──
12
28
  const agentsDir = join(plugin.path, 'agents')
13
29
  if (existsSync(agentsDir)) {
14
- // → register with AgentRegistry
30
+ try {
31
+ agentRegistry.loadDirectory(agentsDir, 'user')
32
+ } catch (err) {
33
+ process.stderr.write(
34
+ `[plugin] Failed to load agents from "${plugin.name}": ${String(err)}\n`,
35
+ )
36
+ }
15
37
  }
16
38
 
17
- // Load custom skills
39
+ // ── Custom skills ──
18
40
  const skillsDir = join(plugin.path, 'skills')
19
41
  if (existsSync(skillsDir)) {
20
- // → register with SkillsLoader
42
+ try {
43
+ skillsLoader.loadExternal([skillsDir])
44
+ } catch (err) {
45
+ process.stderr.write(
46
+ `[plugin] Failed to load skills from "${plugin.name}": ${String(err)}\n`,
47
+ )
48
+ }
21
49
  }
22
50
 
23
- // Load MCP servers
51
+ // ── MCP servers ──
24
52
  const mcpDir = join(plugin.path, 'mcp-servers')
25
53
  if (existsSync(mcpDir)) {
26
- // → register MCP configs
54
+ try {
55
+ const entries = readdirSync(mcpDir)
56
+ for (const entry of entries) {
57
+ if (!entry.endsWith('.json')) continue
58
+ try {
59
+ const raw = readFileSync(join(mcpDir, entry), 'utf-8')
60
+ const cfg = JSON.parse(raw) as McpServerConfig
61
+ if (cfg.name && cfg.command) {
62
+ mcpClient
63
+ .connect(cfg)
64
+ .then(() => {
65
+ const count = registerMcpServerTools(cfg.name, toolsMap)
66
+ if (count > 0) {
67
+ process.stderr.write(
68
+ `[plugin] "${plugin.name}": registered ${count} MCP tools from "${cfg.name}"\n`,
69
+ )
70
+ }
71
+ })
72
+ .catch((err: unknown) => {
73
+ process.stderr.write(
74
+ `[plugin] Failed to connect MCP "${cfg.name}" from "${plugin.name}": ${String(err)}\n`,
75
+ )
76
+ })
77
+ }
78
+ } catch {
79
+ // skip unparseable MCP config files
80
+ }
81
+ }
82
+ } catch (err) {
83
+ process.stderr.write(
84
+ `[plugin] Failed to load MCP configs from "${plugin.name}": ${String(err)}\n`,
85
+ )
86
+ }
27
87
  }
28
88
 
29
- // Load hooks
30
- // → parse plugin.json hooks → register with HookEngine
89
+ // ── Hooks from plugin.json ──
90
+ // Plugin hooks are HookConfig objects (type + command/url), not HookDefinition
91
+ // (which requires a handler function). Convert them via executeHook wrapper.
92
+ try {
93
+ const manifestPath = join(plugin.path, 'plugin.json')
94
+ if (existsSync(manifestPath)) {
95
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
96
+ if (manifest.hooks && Array.isArray(manifest.hooks)) {
97
+ for (const hookCfg of manifest.hooks as HookConfig[]) {
98
+ if (hookCfg.type && hookCfg) {
99
+ const event = (hookCfg as unknown as Record<string, unknown>).event as
100
+ | HookEvent
101
+ | undefined
102
+ if (event) {
103
+ hookEngine.register({
104
+ event,
105
+ handler: async (ctx) => executeHook(hookCfg, ctx),
106
+ })
107
+ }
108
+ }
109
+ }
110
+ }
111
+ }
112
+ } catch (err) {
113
+ process.stderr.write(`[plugin] Failed to load hooks from "${plugin.name}": ${String(err)}\n`)
114
+ }
31
115
  }
32
-
33
- // Suppress unused parameter warnings — these are wiring stubs for future phases
34
- void engine
35
- void skillsLoader
36
116
  }
package/src/ui/app.tsx CHANGED
@@ -97,10 +97,14 @@ export function App({
97
97
 
98
98
  // Initialize agent registry (one-time, on mount)
99
99
  useMemo(() => {
100
- const agentRegistry = new AgentRegistry()
101
- agentRegistry.loadUserAgents()
102
- agentRegistry.loadProjectAgents(process.cwd())
103
- engine.setAgentRegistry(agentRegistry)
100
+ // Agent registry may already be initialized in index.tsx (for plugin loading).
101
+ // Only create a fresh one if not already set on the engine.
102
+ if (!engine.getAgentRegistry()) {
103
+ const agentRegistry = new AgentRegistry()
104
+ agentRegistry.loadUserAgents()
105
+ agentRegistry.loadProjectAgents(process.cwd())
106
+ engine.setAgentRegistry(agentRegistry)
107
+ }
104
108
  }, [])
105
109
 
106
110
  const mkCtx = useCallback(