@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 +1 -1
- package/src/agent/sub-agent.ts +87 -14
- package/src/agent/types.ts +2 -0
- package/src/index.tsx +18 -0
- package/src/plugin/plugin-loader.ts +96 -16
- package/src/ui/app.tsx +8 -4
package/package.json
CHANGED
package/src/agent/sub-agent.ts
CHANGED
|
@@ -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
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
94
|
-
|
|
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')) {
|
package/src/agent/types.ts
CHANGED
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
|
-
|
|
8
|
-
skillsLoader:
|
|
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
|
-
//
|
|
27
|
+
// ── Custom agents ──
|
|
12
28
|
const agentsDir = join(plugin.path, 'agents')
|
|
13
29
|
if (existsSync(agentsDir)) {
|
|
14
|
-
|
|
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
|
-
//
|
|
39
|
+
// ── Custom skills ──
|
|
18
40
|
const skillsDir = join(plugin.path, 'skills')
|
|
19
41
|
if (existsSync(skillsDir)) {
|
|
20
|
-
|
|
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
|
-
//
|
|
51
|
+
// ── MCP servers ──
|
|
24
52
|
const mcpDir = join(plugin.path, 'mcp-servers')
|
|
25
53
|
if (existsSync(mcpDir)) {
|
|
26
|
-
|
|
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
|
-
//
|
|
30
|
-
//
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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(
|