@miphamai/cli 0.6.6 → 0.6.8

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.6",
3
+ "version": "0.6.8",
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
  }
@@ -6,7 +6,8 @@ import type { SkillsLoader } from '../skills/loader'
6
6
  import type { HookEngine } from '../core/hooks'
7
7
  import type { McpClient } from '../mcp/client'
8
8
  import { registerMcpServerTools } from '../mcp/registry'
9
- import type { McpServerConfig, ToolDefinition, HookDefinition } from '../shared/types'
9
+ import { executeHook } from '../core/hooks-executor'
10
+ import type { McpServerConfig, ToolDefinition, HookConfig, HookEvent } from '../shared/types'
10
11
 
11
12
  /**
12
13
  * Activate all enabled plugins — load their agents, skills, MCP servers, and hooks.
@@ -86,14 +87,24 @@ export function loadPlugins(
86
87
  }
87
88
 
88
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.
89
92
  try {
90
93
  const manifestPath = join(plugin.path, 'plugin.json')
91
94
  if (existsSync(manifestPath)) {
92
95
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
93
96
  if (manifest.hooks && Array.isArray(manifest.hooks)) {
94
- for (const hook of manifest.hooks as HookDefinition[]) {
95
- if (hook.event) {
96
- hookEngine.register(hook)
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
+ }
97
108
  }
98
109
  }
99
110
  }
@@ -35,9 +35,15 @@ export const computerUseTool: ToolDefinition = {
35
35
  switch (action) {
36
36
  case 'screenshot': {
37
37
  const result = await takeScreenshot()
38
- return result.success
39
- ? { success: true, content: `Screenshot captured: ${result.data!.slice(0, 100)}...` }
40
- : { success: false, content: '', error: result.error }
38
+ if (!result.success) {
39
+ return { success: false, content: '', error: result.error }
40
+ }
41
+ const dataUrl = result.data!
42
+ const sizeKB = Math.round((dataUrl.length * 0.75) / 1024) // base64 → bytes estimate
43
+ return {
44
+ success: true,
45
+ content: `Screenshot captured (${sizeKB}KB).\n\n${dataUrl}`,
46
+ }
41
47
  }
42
48
  case 'launch': {
43
49
  const result = launchApp(target)