@miphamai/cli 0.4.0 → 0.5.0
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/bin/mipham.ts +186 -0
- package/package.json +1 -1
- package/src/agent/agent-context.ts +56 -0
- package/src/agent/agent-registry.ts +134 -0
- package/src/agent/sub-agent.ts +73 -88
- package/src/agent/types.ts +39 -0
- package/src/agent-view/agent-view-manager.ts +217 -0
- package/src/agent-view/dashboard.tsx +218 -0
- package/src/agent-view/session-peek.tsx +72 -0
- package/src/agent-view/session-row.tsx +70 -0
- package/src/artifacts/server.ts +31 -0
- package/src/artifacts/versioning.ts +127 -0
- package/src/core/context-compact.ts +50 -0
- package/src/core/context-drain.ts +54 -0
- package/src/core/context-microcompact.ts +132 -0
- package/src/core/context-snip.ts +74 -0
- package/src/core/context-token.ts +108 -0
- package/src/core/context.ts +169 -0
- package/src/core/engine.ts +144 -3
- package/src/core/hooks-config.ts +52 -0
- package/src/core/hooks-executor.ts +113 -0
- package/src/core/hooks.ts +90 -30
- package/src/core/instructions.ts +11 -0
- package/src/core/memory/memory-loader.ts +23 -0
- package/src/core/memory/memory-manager.ts +192 -0
- package/src/core/memory/memory-writer.ts +72 -0
- package/src/core/permission-config.ts +34 -0
- package/src/core/permission-rules.ts +66 -0
- package/src/core/permission.ts +221 -36
- package/src/index.tsx +20 -0
- package/src/plugin/plugin-loader.ts +36 -0
- package/src/plugin/plugin-manager.ts +125 -0
- package/src/plugin/plugin-validator.ts +41 -0
- package/src/shared/types.ts +61 -1
- package/src/skills/fork-executor.ts +40 -0
- package/src/skills/loader.ts +46 -0
- package/src/tools/agent/agent.ts +20 -11
- package/src/tools/agent/skill.ts +32 -2
- package/src/tools/computer/app-launcher.ts +39 -0
- package/src/tools/computer/browser.ts +48 -0
- package/src/tools/computer/computer-use.ts +88 -0
- package/src/tools/computer/playwright.d.ts +7 -0
- package/src/tools/computer/screenshot.ts +57 -0
- package/src/tools/index.ts +3 -0
- package/src/ui/app.tsx +10 -1
- package/src/ui/commands.ts +99 -30
- package/src/ui/input.tsx +123 -8
- package/src/ui/vim-motions.ts +96 -0
- package/src/workflow/budget.ts +33 -0
- package/src/workflow/journal.ts +105 -0
- package/src/workflow/primitives/agent.ts +51 -0
- package/src/workflow/primitives/parallel.ts +8 -0
- package/src/workflow/primitives/phase.ts +19 -0
- package/src/workflow/primitives/pipeline.ts +24 -0
- package/src/workflow/runtime.ts +87 -0
- package/src/workflow/sandbox.ts +75 -0
package/src/skills/loader.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
2
2
|
import { join } from 'node:path'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
3
4
|
import { parse as parseYaml } from 'yaml'
|
|
4
5
|
import type { SkillDefinition } from '../shared/index.ts'
|
|
5
6
|
|
|
@@ -94,6 +95,12 @@ export class SkillsLoader {
|
|
|
94
95
|
tools: data.tools as SkillDefinition['tools'],
|
|
95
96
|
hooks: data.hooks as SkillDefinition['hooks'],
|
|
96
97
|
prompts: data.prompts as SkillDefinition['prompts'],
|
|
98
|
+
// NEW: frontmatter fields for fork/auto-trigger support
|
|
99
|
+
context: data.context as string | undefined,
|
|
100
|
+
model: data.model as string | undefined,
|
|
101
|
+
allowedTools: data['allowed-tools'] as string[] | undefined,
|
|
102
|
+
disableModelInvocation: data['disable-model-invocation'] as boolean | undefined,
|
|
103
|
+
userInvocable: data['user-invocable'] as boolean | undefined,
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
this.skills.set(skill.name, skill)
|
|
@@ -102,6 +109,45 @@ export class SkillsLoader {
|
|
|
102
109
|
}
|
|
103
110
|
}
|
|
104
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Build the system-reminder block for AI auto-triggering.
|
|
114
|
+
* Injects available skill names + descriptions so the AI can match
|
|
115
|
+
* user requests to relevant skills.
|
|
116
|
+
*/
|
|
117
|
+
buildSystemReminder(maxTokens: number = 5000): string {
|
|
118
|
+
const skills = this.list().filter((s) => {
|
|
119
|
+
// Skip skills that disable model invocation
|
|
120
|
+
return !s.disableModelInvocation
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
if (skills.length === 0) return ''
|
|
124
|
+
|
|
125
|
+
const lines: string[] = [
|
|
126
|
+
'<system-reminder>',
|
|
127
|
+
'The following skills are available. Invoke via the Skill tool when relevant:',
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
let tokenBudget = 0
|
|
131
|
+
for (const skill of skills) {
|
|
132
|
+
const entry = `- ${skill.name}: ${skill.description}`
|
|
133
|
+
const entryTokens = Math.ceil(entry.length / 4) + 1 // rough estimate
|
|
134
|
+
if (tokenBudget + entryTokens > maxTokens) break
|
|
135
|
+
|
|
136
|
+
lines.push(entry)
|
|
137
|
+
tokenBudget += entryTokens
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
lines.push('</system-reminder>')
|
|
141
|
+
return lines.join('\n')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Load skills from ~/.mipham/skills/ (user home directory). */
|
|
145
|
+
loadUserSkills(): void {
|
|
146
|
+
const home = homedir()
|
|
147
|
+
const userSkillsPath = join(home, '.mipham', 'skills')
|
|
148
|
+
this.loadExternal([userSkillsPath])
|
|
149
|
+
}
|
|
150
|
+
|
|
105
151
|
private nameFromPath(path: string): string {
|
|
106
152
|
const base = path.split('/').pop() || ''
|
|
107
153
|
return base.replace(/\.(SKILL|mipham-skill)\.md$/i, '')
|
package/src/tools/agent/agent.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
-
import { SubAgent
|
|
2
|
+
import { SubAgent } from '../../agent/sub-agent'
|
|
3
|
+
import type { SubAgentType } from '../../agent/types'
|
|
3
4
|
|
|
4
5
|
const VALID_TYPES: SubAgentType[] = ['general', 'explore', 'plan', 'code-review']
|
|
5
6
|
|
|
@@ -35,17 +36,25 @@ export const agentTool: ToolDefinition = {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const registry = ctx.registry
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
const toolRegistry = ctx.toolRegistry
|
|
40
|
+
if (!registry || !toolRegistry) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
content: '',
|
|
44
|
+
error:
|
|
45
|
+
'Sub-agent execution requires an active provider and tool registry. Connect a provider API key first.',
|
|
46
|
+
}
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
49
|
+
// Resolve agent definition from registry (custom > builtin)
|
|
50
|
+
const agentDef = ctx.agentRegistry?.resolve(agentType)
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const sub = new SubAgent(registry, toolRegistry)
|
|
54
|
+
const result = await sub.execute(prompt, description, { type: agentType, agentDef })
|
|
55
|
+
return { success: true, content: result }
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { success: false, content: '', error: String(err) }
|
|
58
|
+
}
|
|
50
59
|
},
|
|
51
60
|
}
|
package/src/tools/agent/skill.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type { ToolDefinition } from '../../shared/index.ts'
|
|
1
|
+
import type { ToolDefinition, SkillDefinition, ToolResult } from '../../shared/index.ts'
|
|
2
|
+
import { executeForkedSkill } from '../../skills/fork-executor'
|
|
2
3
|
|
|
3
4
|
export const skillTool: ToolDefinition = {
|
|
4
5
|
name: 'Skill',
|
|
@@ -34,7 +35,36 @@ export const skillTool: ToolDefinition = {
|
|
|
34
35
|
}
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
//
|
|
38
|
+
// Check if skill has context: fork — execute in isolated subagent
|
|
39
|
+
if (skill.context === 'fork') {
|
|
40
|
+
const registry = ctx.registry
|
|
41
|
+
if (!registry) {
|
|
42
|
+
return {
|
|
43
|
+
success: false,
|
|
44
|
+
content: '',
|
|
45
|
+
error: 'Provider registry not available for forked skill execution.',
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const result = await executeForkedSkill(
|
|
51
|
+
skill,
|
|
52
|
+
args,
|
|
53
|
+
registry,
|
|
54
|
+
ctx.toolRegistry || new Map(),
|
|
55
|
+
)
|
|
56
|
+
// Return to AI as internal context
|
|
57
|
+
return { success: true, content: `[Forked skill "${skillName}" result]:\n${result}` }
|
|
58
|
+
} catch (err) {
|
|
59
|
+
return {
|
|
60
|
+
success: false,
|
|
61
|
+
content: '',
|
|
62
|
+
error: `Forked skill execution failed: ${String(err)}`,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Standard inline execution — return metadata for AI to follow
|
|
38
68
|
const lines: string[] = [
|
|
39
69
|
`── Skill Invoked: ${skill.name} ──`,
|
|
40
70
|
`Type: ${skill.type} | Version: ${skill.version}`,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
const ALLOWED_APPS = new Set([
|
|
4
|
+
'Finder',
|
|
5
|
+
'Safari',
|
|
6
|
+
'Google Chrome',
|
|
7
|
+
'Firefox',
|
|
8
|
+
'Terminal',
|
|
9
|
+
'VS Code',
|
|
10
|
+
'System Settings',
|
|
11
|
+
'Activity Monitor',
|
|
12
|
+
'TextEdit',
|
|
13
|
+
'Preview',
|
|
14
|
+
])
|
|
15
|
+
|
|
16
|
+
export function launchApp(appName: string): { success: boolean; message: string } {
|
|
17
|
+
if (!ALLOWED_APPS.has(appName)) {
|
|
18
|
+
return {
|
|
19
|
+
success: false,
|
|
20
|
+
message: `App "${appName}" is not in the allowed list. Allowed: ${[...ALLOWED_APPS].join(', ')}`,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const platform = process.platform
|
|
26
|
+
if (platform === 'darwin') {
|
|
27
|
+
execSync(`open -a "${appName}"`, { timeout: 5000 })
|
|
28
|
+
} else if (platform === 'linux') {
|
|
29
|
+
execSync(`xdg-open "${appName}"`, { timeout: 5000 })
|
|
30
|
+
} else if (platform === 'win32') {
|
|
31
|
+
execSync(`start "" "${appName}"`, { timeout: 5000, shell: 'cmd.exe' })
|
|
32
|
+
} else {
|
|
33
|
+
return { success: false, message: `Unsupported platform: ${platform}` }
|
|
34
|
+
}
|
|
35
|
+
return { success: true, message: `Launched: ${appName}` }
|
|
36
|
+
} catch (err) {
|
|
37
|
+
return { success: false, message: `Failed to launch ${appName}: ${String(err)}` }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
let browserPromise: Promise<unknown> | null = null
|
|
2
|
+
|
|
3
|
+
async function getPage(): Promise<unknown> {
|
|
4
|
+
if (!browserPromise) {
|
|
5
|
+
// Dynamic import — Playwright is optional
|
|
6
|
+
let playwrightModule: { chromium: unknown }
|
|
7
|
+
try {
|
|
8
|
+
playwrightModule = await import('playwright')
|
|
9
|
+
} catch {
|
|
10
|
+
throw new Error('Playwright is not installed. Install it with: npm install playwright')
|
|
11
|
+
}
|
|
12
|
+
const { chromium } = playwrightModule as {
|
|
13
|
+
chromium: {
|
|
14
|
+
launch: (opts?: Record<string, unknown>) => Promise<{
|
|
15
|
+
newPage: () => Promise<unknown>
|
|
16
|
+
}>
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const browser = await chromium.launch({ headless: false })
|
|
20
|
+
browserPromise = browser.newPage()
|
|
21
|
+
}
|
|
22
|
+
return browserPromise
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function browserNavigate(url: string): Promise<string> {
|
|
26
|
+
const page = (await getPage()) as {
|
|
27
|
+
goto: (u: string) => Promise<void>
|
|
28
|
+
url: () => string
|
|
29
|
+
}
|
|
30
|
+
await page.goto(url)
|
|
31
|
+
return `Navigated to: ${page.url()}`
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function browserSnapshot(): Promise<string> {
|
|
35
|
+
const page = (await getPage()) as {
|
|
36
|
+
accessibility: { snapshot: () => Promise<unknown> }
|
|
37
|
+
}
|
|
38
|
+
const snapshot = await page.accessibility.snapshot()
|
|
39
|
+
return JSON.stringify(snapshot, null, 2)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function browserClick(uid: string): Promise<string> {
|
|
43
|
+
const page = (await getPage()) as {
|
|
44
|
+
locator: (s: string) => { click: () => Promise<void> }
|
|
45
|
+
}
|
|
46
|
+
await page.locator(`[uid="${uid}"]`).click()
|
|
47
|
+
return `Clicked: ${uid}`
|
|
48
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
2
|
+
import { takeScreenshot } from './screenshot'
|
|
3
|
+
import { launchApp } from './app-launcher'
|
|
4
|
+
import { browserNavigate, browserSnapshot, browserClick } from './browser'
|
|
5
|
+
|
|
6
|
+
export const computerUseTool: ToolDefinition = {
|
|
7
|
+
name: 'ComputerUse',
|
|
8
|
+
description:
|
|
9
|
+
'Take screenshots, launch apps, or control a browser. Always requires user approval.',
|
|
10
|
+
category: 'system',
|
|
11
|
+
permission: 'ask', // ALWAYS ask — security requirement
|
|
12
|
+
parameters: {
|
|
13
|
+
type: 'object',
|
|
14
|
+
properties: {
|
|
15
|
+
action: {
|
|
16
|
+
type: 'string',
|
|
17
|
+
enum: ['screenshot', 'launch', 'browser_navigate', 'browser_snapshot', 'browser_click'],
|
|
18
|
+
description: 'The computer-use action to perform',
|
|
19
|
+
},
|
|
20
|
+
target: {
|
|
21
|
+
type: 'string',
|
|
22
|
+
description: 'App name (launch), URL (browser_navigate), or element UID (browser_click)',
|
|
23
|
+
},
|
|
24
|
+
text: {
|
|
25
|
+
type: 'string',
|
|
26
|
+
description: 'Text to type (reserved for future browser_type action)',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
required: ['action'],
|
|
30
|
+
},
|
|
31
|
+
async execute(params, _ctx) {
|
|
32
|
+
const action = params.action as string
|
|
33
|
+
const target = (params.target as string) || ''
|
|
34
|
+
|
|
35
|
+
switch (action) {
|
|
36
|
+
case 'screenshot': {
|
|
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 }
|
|
41
|
+
}
|
|
42
|
+
case 'launch': {
|
|
43
|
+
const result = launchApp(target)
|
|
44
|
+
return result.success
|
|
45
|
+
? { success: true, content: result.message }
|
|
46
|
+
: { success: false, content: '', error: result.message }
|
|
47
|
+
}
|
|
48
|
+
case 'browser_navigate': {
|
|
49
|
+
try {
|
|
50
|
+
const url = await browserNavigate(target)
|
|
51
|
+
return { success: true, content: url }
|
|
52
|
+
} catch (err) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
content: '',
|
|
56
|
+
error: `Browser navigation failed: ${String(err)}`,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
case 'browser_snapshot': {
|
|
61
|
+
try {
|
|
62
|
+
const snapshot = await browserSnapshot()
|
|
63
|
+
return { success: true, content: snapshot }
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return {
|
|
66
|
+
success: false,
|
|
67
|
+
content: '',
|
|
68
|
+
error: `Browser snapshot failed: ${String(err)}`,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
case 'browser_click': {
|
|
73
|
+
try {
|
|
74
|
+
const result = await browserClick(target)
|
|
75
|
+
return { success: true, content: result }
|
|
76
|
+
} catch (err) {
|
|
77
|
+
return {
|
|
78
|
+
success: false,
|
|
79
|
+
content: '',
|
|
80
|
+
error: `Browser click failed: ${String(err)}`,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
return { success: false, content: '', error: `Unknown action: ${action}` }
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { tmpdir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
export async function takeScreenshot(): Promise<{
|
|
7
|
+
success: boolean
|
|
8
|
+
data?: string
|
|
9
|
+
error?: string
|
|
10
|
+
}> {
|
|
11
|
+
const tmpPath = join(tmpdir(), `mipham-screenshot-${Date.now()}.png`)
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const platform = process.platform
|
|
15
|
+
if (platform === 'darwin') {
|
|
16
|
+
execSync(`screencapture -x ${tmpPath}`, { timeout: 10000 })
|
|
17
|
+
} else if (platform === 'linux') {
|
|
18
|
+
execSync(`import -window root ${tmpPath}`, { timeout: 10000 })
|
|
19
|
+
} else if (platform === 'win32') {
|
|
20
|
+
// PowerShell script to capture primary screen and save as PNG
|
|
21
|
+
const psScript = `
|
|
22
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
23
|
+
Add-Type -AssemblyName System.Drawing
|
|
24
|
+
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
|
|
25
|
+
$bitmap = New-Object System.Drawing.Bitmap($screen.Bounds.Width, $screen.Bounds.Height)
|
|
26
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
|
|
27
|
+
$graphics.CopyFromScreen($screen.Bounds.X, $screen.Bounds.Y, 0, 0, $bitmap.Size)
|
|
28
|
+
$bitmap.Save('${tmpPath.replace(/\\/g, '\\\\')}', [System.Drawing.Imaging.ImageFormat]::Png)
|
|
29
|
+
$graphics.Dispose()
|
|
30
|
+
$bitmap.Dispose()
|
|
31
|
+
`.trim()
|
|
32
|
+
const psPath = join(tmpdir(), `mipham-sc-${Date.now()}.ps1`)
|
|
33
|
+
writeFileSync(psPath, psScript, 'utf-8')
|
|
34
|
+
execSync(`powershell -ExecutionPolicy Bypass -File "${psPath}"`, { timeout: 15000 })
|
|
35
|
+
try {
|
|
36
|
+
unlinkSync(psPath)
|
|
37
|
+
} catch {
|
|
38
|
+
// ignore cleanup failures
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = readFileSync(tmpPath, 'base64')
|
|
43
|
+
try {
|
|
44
|
+
unlinkSync(tmpPath)
|
|
45
|
+
} catch {
|
|
46
|
+
// ignore cleanup failures
|
|
47
|
+
}
|
|
48
|
+
return { success: true, data: `data:image/png;base64,${data}` }
|
|
49
|
+
} catch (err) {
|
|
50
|
+
try {
|
|
51
|
+
unlinkSync(tmpPath)
|
|
52
|
+
} catch {
|
|
53
|
+
// ignore cleanup failures
|
|
54
|
+
}
|
|
55
|
+
return { success: false, error: `Screenshot failed: ${String(err)}` }
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { webSearchTool } from './network/web-search'
|
|
|
16
16
|
import { configTool } from './system/config'
|
|
17
17
|
import { mcpTool } from './system/mcp'
|
|
18
18
|
import { artifactTool } from './artifact/artifact'
|
|
19
|
+
import { computerUseTool } from './computer/computer-use'
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* Validate tool parameters against the tool's JSON Schema definition.
|
|
@@ -119,6 +120,8 @@ export function createToolRegistry(): Map<string, ToolDefinition> {
|
|
|
119
120
|
withValidation(mcpTool),
|
|
120
121
|
// Artifact tools
|
|
121
122
|
withValidation(artifactTool),
|
|
123
|
+
// Computer Use tools
|
|
124
|
+
withValidation(computerUseTool),
|
|
122
125
|
]
|
|
123
126
|
|
|
124
127
|
const map = new Map<string, ToolDefinition>()
|
package/src/ui/app.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import React, { useState, useCallback, useRef } from 'react'
|
|
1
|
+
import React, { useState, useCallback, useRef, useMemo } from 'react'
|
|
2
2
|
import { Box, Text, useInput } from 'ink'
|
|
3
3
|
import type { QueryEngine } from '../core/engine'
|
|
4
4
|
import type { MiphamConfig } from '../shared/index.ts'
|
|
5
5
|
import type { SkillsLoader } from '../skills/loader'
|
|
6
|
+
import { AgentRegistry } from '../agent/agent-registry'
|
|
6
7
|
import { ChatPanel } from './chat'
|
|
7
8
|
import { InputBar } from './input'
|
|
8
9
|
import { ModelPicker } from './picker'
|
|
@@ -88,6 +89,14 @@ export function App({
|
|
|
88
89
|
return () => clearInterval(interval)
|
|
89
90
|
}, [agentProgress])
|
|
90
91
|
|
|
92
|
+
// Initialize agent registry (one-time, on mount)
|
|
93
|
+
useMemo(() => {
|
|
94
|
+
const agentRegistry = new AgentRegistry()
|
|
95
|
+
agentRegistry.loadUserAgents()
|
|
96
|
+
agentRegistry.loadProjectAgents(process.cwd())
|
|
97
|
+
engine.setAgentRegistry(agentRegistry)
|
|
98
|
+
}, [])
|
|
99
|
+
|
|
91
100
|
const mkCtx = useCallback(
|
|
92
101
|
(): CommandContext => ({
|
|
93
102
|
engine,
|
package/src/ui/commands.ts
CHANGED
|
@@ -126,7 +126,10 @@ const helpCmd: CommandHandler = (_ctx) => ({
|
|
|
126
126
|
/login Show API key status
|
|
127
127
|
/logout Clear credentials guide
|
|
128
128
|
/feedback Send feedback
|
|
129
|
-
|
|
129
|
+
|
|
130
|
+
── Agents ──────────────────────────
|
|
131
|
+
/agents Agent view dashboard
|
|
132
|
+
/bg <prompt> Run a background agent task
|
|
130
133
|
|
|
131
134
|
Type /exit or Esc to quit.
|
|
132
135
|
`,
|
|
@@ -419,6 +422,7 @@ const goalCmd: CommandHandler = (ctx, args) => {
|
|
|
419
422
|
}
|
|
420
423
|
}
|
|
421
424
|
ctx.setGoal(goal.trim())
|
|
425
|
+
ctx.engine.setGoal(goal.trim())
|
|
422
426
|
return {
|
|
423
427
|
content: `✓ Goal set: "${goal.trim()}"\n\nUse /status to view progress. Type /goal without arguments to clear.`,
|
|
424
428
|
}
|
|
@@ -2078,47 +2082,111 @@ const feedbackCmd: CommandHandler = (ctx, args) => {
|
|
|
2078
2082
|
// Phase 4 — Agent Management
|
|
2079
2083
|
// ═══════════════════════════════════════════════════════════════
|
|
2080
2084
|
|
|
2081
|
-
const agentsCmd: CommandHandler = () =>
|
|
2082
|
-
|
|
2085
|
+
const agentsCmd: CommandHandler = (ctx) => {
|
|
2086
|
+
const agentViewManager = ctx.engine.getAgentViewManager?.()
|
|
2087
|
+
if (!agentViewManager) {
|
|
2088
|
+
return { content: 'Agent View dashboard is initializing...' }
|
|
2089
|
+
}
|
|
2090
|
+
const counts = agentViewManager.countByStatus()
|
|
2091
|
+
const total = counts['needs-input'] + counts.working + counts.completed + counts.failed
|
|
2092
|
+
|
|
2093
|
+
if (total === 0) {
|
|
2094
|
+
return {
|
|
2095
|
+
content: `── Agent View ──
|
|
2096
|
+
|
|
2097
|
+
No background agents running.
|
|
2098
|
+
|
|
2099
|
+
Use /bg <prompt> to spawn a background agent session.
|
|
2100
|
+
|
|
2101
|
+
Example:
|
|
2102
|
+
/bg Fix all TypeScript errors in the project
|
|
2103
|
+
/bg Review the auth module for security issues
|
|
2104
|
+
|
|
2105
|
+
Or use the Agent tool in a conversation to launch a sub-agent.`,
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
const lines: string[] = [
|
|
2110
|
+
'── Agent View ──',
|
|
2111
|
+
'',
|
|
2112
|
+
` ${total} session(s):`,
|
|
2113
|
+
` 🟡 Needs input: ${counts['needs-input']}`,
|
|
2114
|
+
` 🔵 Working: ${counts.working}`,
|
|
2115
|
+
` 🟢 Completed: ${counts.completed}`,
|
|
2116
|
+
` 🔴 Failed: ${counts.failed}`,
|
|
2117
|
+
'',
|
|
2118
|
+
'Sessions:',
|
|
2119
|
+
]
|
|
2120
|
+
|
|
2121
|
+
const all = agentViewManager.list()
|
|
2122
|
+
for (const s of all.slice(0, 10)) {
|
|
2123
|
+
const statusIcon: Record<string, string> = {
|
|
2124
|
+
'needs-input': '🟡',
|
|
2125
|
+
working: '🔵',
|
|
2126
|
+
completed: '🟢',
|
|
2127
|
+
failed: '🔴',
|
|
2128
|
+
}
|
|
2129
|
+
const elapsed =
|
|
2130
|
+
s.elapsedMs < 1000
|
|
2131
|
+
? '<1s'
|
|
2132
|
+
: s.elapsedMs < 60000
|
|
2133
|
+
? `${Math.floor(s.elapsedMs / 1000)}s`
|
|
2134
|
+
: `${Math.floor(s.elapsedMs / 60000)}m`
|
|
2135
|
+
lines.push(
|
|
2136
|
+
` ${statusIcon[s.status] || '⚪'} ${s.id} · ${s.provider}/${s.model} · ${elapsed} · ${s.task.slice(0, 50)}`,
|
|
2137
|
+
)
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
if (total > 10) {
|
|
2141
|
+
lines.push(` ... and ${total - 10} more`)
|
|
2142
|
+
}
|
|
2083
2143
|
|
|
2084
|
-
|
|
2144
|
+
lines.push('', 'Run `mipham agents` for the full interactive dashboard with j/k navigation.')
|
|
2085
2145
|
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
├──────────┼──────────┼────────────┼────────────────────────────────┤
|
|
2089
|
-
│ Agent │ agent │ ask │ description*, prompt*, │
|
|
2090
|
-
│ │ │ │ subagent_type (optional) │
|
|
2091
|
-
│ Skill │ agent │ auto │ skill*, args (optional) │
|
|
2092
|
-
│ Plan │ agent │ auto │ (no required params) │
|
|
2093
|
-
│ Memory │ agent │ auto │ action* (read|write|list), │
|
|
2094
|
-
│ │ │ │ name, content │
|
|
2095
|
-
└──────────┴──────────┴────────────┴────────────────────────────────┘
|
|
2146
|
+
return { content: lines.join('\n') }
|
|
2147
|
+
}
|
|
2096
2148
|
|
|
2097
|
-
|
|
2149
|
+
const bgCmd: CommandHandler = (ctx, args) => {
|
|
2150
|
+
const prompt = args.join(' ')
|
|
2151
|
+
if (!prompt.trim()) {
|
|
2152
|
+
return {
|
|
2153
|
+
content: `Usage: /bg <prompt>
|
|
2098
2154
|
|
|
2099
|
-
|
|
2100
|
-
Types: general-purpose, Explore, Plan, code-reviewer, etc.
|
|
2155
|
+
Spawn a background agent to work on a task while you continue chatting.
|
|
2101
2156
|
|
|
2102
|
-
|
|
2103
|
-
|
|
2157
|
+
Examples:
|
|
2158
|
+
/bg Run the full test suite and report failures
|
|
2159
|
+
/bg Audit the src/ directory for security issues
|
|
2160
|
+
/bg Generate API documentation from JSDoc comments
|
|
2104
2161
|
|
|
2105
|
-
|
|
2106
|
-
|
|
2162
|
+
Background agents appear in the Agent View dashboard (/agents).`,
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2107
2165
|
|
|
2108
|
-
|
|
2109
|
-
|
|
2166
|
+
const agentViewManager = ctx.engine.getAgentViewManager?.()
|
|
2167
|
+
if (!agentViewManager) {
|
|
2168
|
+
return { content: 'Agent View manager is initializing...' }
|
|
2169
|
+
}
|
|
2110
2170
|
|
|
2111
|
-
|
|
2171
|
+
const session = agentViewManager.create(prompt, prompt, {
|
|
2172
|
+
provider: ctx.providerId,
|
|
2173
|
+
model: ctx.modelId,
|
|
2174
|
+
})
|
|
2112
2175
|
|
|
2113
|
-
|
|
2176
|
+
agentViewManager.addMessage(session.id, { role: 'user', content: prompt })
|
|
2177
|
+
agentViewManager.updateStatus(session.id, 'working')
|
|
2114
2178
|
|
|
2115
|
-
|
|
2116
|
-
|
|
2179
|
+
return {
|
|
2180
|
+
content: `✓ Background agent spawned: ${session.id}
|
|
2117
2181
|
|
|
2118
|
-
|
|
2182
|
+
Task: ${session.task.slice(0, 80)}
|
|
2183
|
+
Provider: ${session.provider} / ${session.model}
|
|
2184
|
+
Status: working
|
|
2119
2185
|
|
|
2120
|
-
|
|
2121
|
-
|
|
2186
|
+
Use /agents to view all background sessions.
|
|
2187
|
+
Run \`mipham agents\` for the interactive dashboard.`,
|
|
2188
|
+
}
|
|
2189
|
+
}
|
|
2122
2190
|
|
|
2123
2191
|
// ═══════════════════════════════════════════════════════════════
|
|
2124
2192
|
// Artifacts
|
|
@@ -2286,6 +2354,7 @@ registry.set('/login', loginCmd)
|
|
|
2286
2354
|
registry.set('/logout', logoutCmd)
|
|
2287
2355
|
registry.set('/feedback', feedbackCmd)
|
|
2288
2356
|
registry.set('/agents', agentsCmd)
|
|
2357
|
+
registry.set('/bg', bgCmd)
|
|
2289
2358
|
|
|
2290
2359
|
// Artifacts
|
|
2291
2360
|
registry.set('/artifact', artifactBaseCmd)
|