@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.
Files changed (56) hide show
  1. package/bin/mipham.ts +186 -0
  2. package/package.json +1 -1
  3. package/src/agent/agent-context.ts +56 -0
  4. package/src/agent/agent-registry.ts +134 -0
  5. package/src/agent/sub-agent.ts +73 -88
  6. package/src/agent/types.ts +39 -0
  7. package/src/agent-view/agent-view-manager.ts +217 -0
  8. package/src/agent-view/dashboard.tsx +218 -0
  9. package/src/agent-view/session-peek.tsx +72 -0
  10. package/src/agent-view/session-row.tsx +70 -0
  11. package/src/artifacts/server.ts +31 -0
  12. package/src/artifacts/versioning.ts +127 -0
  13. package/src/core/context-compact.ts +50 -0
  14. package/src/core/context-drain.ts +54 -0
  15. package/src/core/context-microcompact.ts +132 -0
  16. package/src/core/context-snip.ts +74 -0
  17. package/src/core/context-token.ts +108 -0
  18. package/src/core/context.ts +169 -0
  19. package/src/core/engine.ts +144 -3
  20. package/src/core/hooks-config.ts +52 -0
  21. package/src/core/hooks-executor.ts +113 -0
  22. package/src/core/hooks.ts +90 -30
  23. package/src/core/instructions.ts +11 -0
  24. package/src/core/memory/memory-loader.ts +23 -0
  25. package/src/core/memory/memory-manager.ts +192 -0
  26. package/src/core/memory/memory-writer.ts +72 -0
  27. package/src/core/permission-config.ts +34 -0
  28. package/src/core/permission-rules.ts +66 -0
  29. package/src/core/permission.ts +221 -36
  30. package/src/index.tsx +20 -0
  31. package/src/plugin/plugin-loader.ts +36 -0
  32. package/src/plugin/plugin-manager.ts +125 -0
  33. package/src/plugin/plugin-validator.ts +41 -0
  34. package/src/shared/types.ts +61 -1
  35. package/src/skills/fork-executor.ts +40 -0
  36. package/src/skills/loader.ts +46 -0
  37. package/src/tools/agent/agent.ts +20 -11
  38. package/src/tools/agent/skill.ts +32 -2
  39. package/src/tools/computer/app-launcher.ts +39 -0
  40. package/src/tools/computer/browser.ts +48 -0
  41. package/src/tools/computer/computer-use.ts +88 -0
  42. package/src/tools/computer/playwright.d.ts +7 -0
  43. package/src/tools/computer/screenshot.ts +57 -0
  44. package/src/tools/index.ts +3 -0
  45. package/src/ui/app.tsx +10 -1
  46. package/src/ui/commands.ts +99 -30
  47. package/src/ui/input.tsx +123 -8
  48. package/src/ui/vim-motions.ts +96 -0
  49. package/src/workflow/budget.ts +33 -0
  50. package/src/workflow/journal.ts +105 -0
  51. package/src/workflow/primitives/agent.ts +51 -0
  52. package/src/workflow/primitives/parallel.ts +8 -0
  53. package/src/workflow/primitives/phase.ts +19 -0
  54. package/src/workflow/primitives/pipeline.ts +24 -0
  55. package/src/workflow/runtime.ts +87 -0
  56. package/src/workflow/sandbox.ts +75 -0
@@ -1,61 +1,127 @@
1
1
  import type {
2
2
  ToolDefinition,
3
- ToolPermission,
3
+ PermissionMode,
4
4
  PermissionLevel,
5
5
  PermissionRule,
6
6
  } from '../shared/index.ts'
7
+ import type { PermissionRuleEntry } from '../shared/index.ts'
8
+ import { matchBashRule, compileRule } from './permission-rules'
9
+ import { loadPermissionConfig, nextMode, MODE_CYCLE } from './permission-config'
10
+
11
+ const VALID_MODES: Set<string> = new Set<string>(MODE_CYCLE)
7
12
 
8
13
  export class PermissionSystem {
9
- private rules = new Map<string, PermissionLevel>()
10
- private patternRules: Array<{ pattern: RegExp; level: PermissionLevel }> = []
14
+ private allowRules: PermissionRuleEntry[] = []
15
+ private denyRules: PermissionRuleEntry[] = []
16
+ private askRules: PermissionRuleEntry[] = []
17
+ /** Legacy exact-name rules for backward compat (set via setRule with 'auto' level). */
18
+ private legacyRules = new Map<string, PermissionLevel>()
19
+ /** Legacy default level from constructor when passed non-mode values like 'ask' or 'bypass'. */
20
+ private legacyDefaultFallback: PermissionLevel | null = null
21
+ private mode: PermissionMode = 'default'
11
22
 
12
- constructor(private defaultLevel: ToolPermission = 'auto') {}
23
+ constructor(modeOrLevel: PermissionLevel = 'default') {
24
+ if (VALID_MODES.has(modeOrLevel)) {
25
+ this.mode = modeOrLevel as PermissionMode
26
+ } else {
27
+ // Legacy values ('ask', 'bypass') → store as fallback, use 'default' mode
28
+ this.mode = 'default'
29
+ this.legacyDefaultFallback = modeOrLevel
30
+ }
31
+ }
13
32
 
14
- setDefaultLevel(level: PermissionLevel): void {
15
- this.defaultLevel = level
33
+ // ── Mode management ──
34
+
35
+ setMode(mode: PermissionMode): void {
36
+ this.mode = mode
16
37
  }
17
38
 
18
- getDefaultLevel(): PermissionLevel {
19
- return this.defaultLevel
39
+ getMode(): PermissionMode {
40
+ return this.mode
20
41
  }
21
42
 
22
- setRule(toolNameOrRule: string | PermissionRule, level?: PermissionLevel): void {
23
- if (typeof toolNameOrRule === 'string') {
24
- if (level) this.rules.set(toolNameOrRule, level)
25
- } else {
26
- const rule = toolNameOrRule
27
- if (rule.pattern) {
28
- this.patternRules.push({ pattern: new RegExp(rule.pattern), level: rule.level })
29
- } else {
30
- this.rules.set(rule.toolName, rule.level)
31
- }
32
- }
43
+ cycleMode(): PermissionMode {
44
+ this.mode = nextMode(this.mode)
45
+ return this.mode
33
46
  }
34
47
 
35
- removeRule(toolName: string): void {
36
- this.rules.delete(toolName)
48
+ // ── Rule management ──
49
+
50
+ allow(rule: string): void {
51
+ this.allowRules.push(compileRule(rule, 'allow'))
52
+ }
53
+
54
+ deny(rule: string): void {
55
+ this.denyRules.push(compileRule(rule, 'deny'))
37
56
  }
38
57
 
58
+ ask(rule: string): void {
59
+ this.askRules.push(compileRule(rule, 'ask'))
60
+ }
61
+
62
+ loadConfig(raw: { mode?: string; allow?: string[]; deny?: string[] }): void {
63
+ const config = loadPermissionConfig(
64
+ raw as Partial<{ mode: PermissionMode; allow: string[]; deny: string[] }>,
65
+ )
66
+ this.mode = config.mode
67
+
68
+ this.allowRules = []
69
+ this.denyRules = []
70
+ this.askRules = []
71
+
72
+ for (const rule of config.allow) {
73
+ this.allowRules.push(compileRule(rule, 'allow'))
74
+ }
75
+ for (const rule of config.deny) {
76
+ this.denyRules.push(compileRule(rule, 'deny'))
77
+ }
78
+ }
79
+
80
+ // ── Permission check ──
81
+
39
82
  /**
40
- * Resolve permission level for a tool call.
41
- *
42
- * Fallback chain: exact rule pattern rule → tool permission → system default
83
+ * Resolution chain (first match wins):
84
+ * 1. Deny rules → block
85
+ * 2. Ask rulesrequire approval
86
+ * 3. Allow rules → permit
87
+ * 4. Legacy exact-name rules (backward compat — e.g. setRule('tool', 'auto'))
88
+ * 5. Mode baseline → mode-specific default (overrides tool.permission for explicit modes)
89
+ * 6. Tool's own permission → tool-specific default (backward compat)
90
+ * 7. Legacy constructor fallback (when constructed with 'ask'/'bypass')
91
+ * 8. System default → 'ask'
43
92
  */
44
- check(tool: ToolDefinition, _input: Record<string, unknown>): PermissionLevel {
45
- // 1. Exact-name rule wins
46
- const ruleLevel = this.rules.get(tool.name)
47
- if (ruleLevel) return ruleLevel
48
-
49
- // 2. Pattern-based rules (e.g. "file_*" → bypass)
50
- for (const { pattern, level } of this.patternRules) {
51
- if (pattern.test(tool.name)) return level
93
+ check(tool: ToolDefinition, input: Record<string, unknown>): PermissionLevel {
94
+ // 1. Check deny rules (always win)
95
+ for (const rule of this.denyRules) {
96
+ if (this.ruleMatches(rule, tool, input)) return 'ask'
52
97
  }
53
98
 
54
- // 3. Tool's own permission level
99
+ // 2. Check ask rules
100
+ for (const rule of this.askRules) {
101
+ if (this.ruleMatches(rule, tool, input)) return 'ask'
102
+ }
103
+
104
+ // 3. Check allow rules
105
+ for (const rule of this.allowRules) {
106
+ if (this.ruleMatches(rule, tool, input)) return 'bypass'
107
+ }
108
+
109
+ // 4. Legacy exact-name rules (backward compat)
110
+ const legacyLevel = this.legacyRules.get(tool.name)
111
+ if (legacyLevel !== undefined) return legacyLevel
112
+
113
+ // 5. Mode baseline
114
+ const baseline = this.modeBaseline(tool)
115
+ if (baseline !== 'mode-baseline') return baseline
116
+
117
+ // 6. Tool's own permission level (backward compat fallback)
55
118
  if (tool.permission) return tool.permission
56
119
 
57
- // 4. System default
58
- return this.defaultLevel
120
+ // 7. Legacy constructor fallback
121
+ if (this.legacyDefaultFallback) return this.legacyDefaultFallback
122
+
123
+ // 8. System default
124
+ return 'ask'
59
125
  }
60
126
 
61
127
  needsApproval(tool: ToolDefinition, input: Record<string, unknown>): boolean {
@@ -66,8 +132,127 @@ export class PermissionSystem {
66
132
  return this.check(tool, input) === 'bypass'
67
133
  }
68
134
 
135
+ // ── Helpers ──
136
+
137
+ private ruleMatches(
138
+ rule: PermissionRuleEntry,
139
+ tool: ToolDefinition,
140
+ input: Record<string, unknown>,
141
+ ): boolean {
142
+ // Try Bash-style matching first
143
+ if (rule.pattern.includes('(')) {
144
+ return matchBashRule(rule.pattern, tool.name, input)
145
+ }
146
+ // Simple tool name match
147
+ return rule.pattern === tool.name || rule.compiled.test(tool.name)
148
+ }
149
+
150
+ private modeBaseline(tool: ToolDefinition): PermissionLevel | 'mode-baseline' {
151
+ switch (this.mode) {
152
+ case 'default':
153
+ // Delegate to tool.permission (backward compat)
154
+ return 'mode-baseline'
155
+
156
+ case 'acceptEdits':
157
+ // Reads + file edits free; Bash requires approval
158
+ return tool.category === 'file'
159
+ ? ['Bash'].includes(tool.name)
160
+ ? 'ask'
161
+ : 'bypass'
162
+ : tool.name === 'Bash'
163
+ ? 'ask'
164
+ : 'ask'
165
+
166
+ case 'plan':
167
+ // Only reads, no writes or executes
168
+ return tool.category === 'file' && ['Read', 'Grep', 'Glob'].includes(tool.name)
169
+ ? 'bypass'
170
+ : 'ask'
171
+
172
+ case 'auto':
173
+ // Respect tool-level permissions; safety checks handled by hook layer
174
+ return 'mode-baseline'
175
+
176
+ case 'dontAsk':
177
+ // Only allowlisted tools free (already handled above); everything else requires approval
178
+ return 'ask'
179
+
180
+ case 'bypassPermissions':
181
+ return 'bypass'
182
+
183
+ default:
184
+ return 'mode-baseline'
185
+ }
186
+ }
187
+
188
+ // ── Legacy compatibility ──
189
+
190
+ setDefaultLevel(level: PermissionLevel): void {
191
+ // Map legacy 3-level to new mode
192
+ if (level === 'auto') this.mode = 'auto'
193
+ else if (level === 'bypass') this.mode = 'bypassPermissions'
194
+ else this.mode = 'default'
195
+ }
196
+
197
+ getDefaultLevel(): PermissionLevel {
198
+ // Legacy constructor fallback takes priority
199
+ if (this.legacyDefaultFallback) return this.legacyDefaultFallback
200
+ if (this.mode === 'auto' || this.mode === 'bypassPermissions' || this.mode === 'dontAsk')
201
+ return 'bypass'
202
+ if (this.mode === 'plan') return 'ask'
203
+ return 'auto'
204
+ }
205
+
206
+ setRule(toolNameOrRule: string | PermissionRule, level?: PermissionLevel): void {
207
+ if (typeof toolNameOrRule === 'string') {
208
+ const toolName = toolNameOrRule
209
+ // Remove old entries for this tool
210
+ this.removeRuleFromArrays(toolName)
211
+ if (level !== undefined) {
212
+ this.legacyRules.set(toolName, level)
213
+ // Also sync to new-style arrays for listRules / new API consistency
214
+ if (level === 'bypass') this.allow(toolName)
215
+ else if (level === 'ask') this.ask(toolName)
216
+ // 'auto' is stored only in legacyRules (returns 'auto', not 'bypass')
217
+ }
218
+ } else {
219
+ const rule = toolNameOrRule
220
+ if (rule.pattern) {
221
+ const entry = compileRule(rule.pattern, rule.level === 'bypass' ? 'allow' : 'ask')
222
+ if (rule.level === 'bypass') this.allowRules.push(entry)
223
+ else this.askRules.push(entry)
224
+ } else {
225
+ this.removeRuleFromArrays(rule.toolName)
226
+ this.legacyRules.set(rule.toolName, rule.level)
227
+ if (rule.level === 'bypass') this.allow(rule.toolName)
228
+ else if (rule.level === 'ask') this.ask(rule.toolName)
229
+ }
230
+ }
231
+ }
232
+
233
+ removeRule(toolName: string): void {
234
+ this.legacyRules.delete(toolName)
235
+ this.removeRuleFromArrays(toolName)
236
+ }
237
+
238
+ private removeRuleFromArrays(toolName: string): void {
239
+ this.allowRules = this.allowRules.filter((r) => r.pattern !== toolName)
240
+ this.denyRules = this.denyRules.filter((r) => r.pattern !== toolName)
241
+ this.askRules = this.askRules.filter((r) => r.pattern !== toolName)
242
+ }
243
+
69
244
  listRules(): Map<string, PermissionLevel> {
70
- return new Map(this.rules)
245
+ const map = new Map<string, PermissionLevel>(this.legacyRules)
246
+ for (const r of this.allowRules) {
247
+ if (!map.has(r.pattern)) map.set(r.pattern, 'bypass')
248
+ }
249
+ for (const r of this.denyRules) {
250
+ if (!map.has(r.pattern)) map.set(r.pattern, 'ask')
251
+ }
252
+ for (const r of this.askRules) {
253
+ if (!map.has(r.pattern)) map.set(r.pattern, 'ask')
254
+ }
255
+ return map
71
256
  }
72
257
 
73
258
  getByCategory(
package/src/index.tsx CHANGED
@@ -13,6 +13,8 @@ import { McpClient } from './mcp/client'
13
13
  import { HookEngine } from './core/hooks'
14
14
  import { ArtifactServer } from './artifacts/server'
15
15
  import { ARTIFACTS_DIR, ARTIFACT_PORT, MIPHAM_DIR } from './shared/constants'
16
+ import { AgentViewManager } from './agent-view/agent-view-manager'
17
+ import { AgentViewDashboard } from './agent-view/dashboard'
16
18
 
17
19
  interface RunOptions {
18
20
  model?: string
@@ -23,6 +25,23 @@ interface RunOptions {
23
25
  }
24
26
 
25
27
  export async function runApp(options: RunOptions): Promise<void> {
28
+ // Handle `mipham agents` subcommand — launch standalone dashboard
29
+ const args = process.argv.slice(2)
30
+ if (args[0] === 'agents') {
31
+ const agentViewManager = new AgentViewManager()
32
+ const { waitUntilExit } = render(
33
+ <AgentViewDashboard
34
+ manager={agentViewManager}
35
+ onAttach={() => {}}
36
+ onExit={() => process.exit(0)}
37
+ />,
38
+ )
39
+ await waitUntilExit()
40
+ process.exit(0)
41
+ }
42
+
43
+ // Create AgentViewManager for the full app (shared across the session)
44
+ const agentViewManager = new AgentViewManager()
26
45
  // Set terminal window title
27
46
  process.stdout.write('\x1b]0;Mipham Code\x07')
28
47
 
@@ -94,6 +113,7 @@ export async function runApp(options: RunOptions): Promise<void> {
94
113
  const engine = new QueryEngine(registry, context, tools)
95
114
  engine.setHookEngine(hookEngine)
96
115
  engine.setArtifactServer(artifactServer)
116
+ engine.setAgentViewManager(agentViewManager)
97
117
  engine.setupContextSummarizer()
98
118
 
99
119
  // Auto-save session on exit
@@ -0,0 +1,36 @@
1
+ import type { PluginManager } from './plugin-manager'
2
+ import { join } from 'node:path'
3
+ import { existsSync } from 'node:fs'
4
+
5
+ export function loadPlugins(
6
+ pluginManager: PluginManager,
7
+ engine: unknown,
8
+ skillsLoader: unknown,
9
+ ): void {
10
+ for (const plugin of pluginManager.getEnabled()) {
11
+ // Load custom agents
12
+ const agentsDir = join(plugin.path, 'agents')
13
+ if (existsSync(agentsDir)) {
14
+ // → register with AgentRegistry
15
+ }
16
+
17
+ // Load custom skills
18
+ const skillsDir = join(plugin.path, 'skills')
19
+ if (existsSync(skillsDir)) {
20
+ // → register with SkillsLoader
21
+ }
22
+
23
+ // Load MCP servers
24
+ const mcpDir = join(plugin.path, 'mcp-servers')
25
+ if (existsSync(mcpDir)) {
26
+ // → register MCP configs
27
+ }
28
+
29
+ // Load hooks
30
+ // → parse plugin.json hooks → register with HookEngine
31
+ }
32
+
33
+ // Suppress unused parameter warnings — these are wiring stubs for future phases
34
+ void engine
35
+ void skillsLoader
36
+ }
@@ -0,0 +1,125 @@
1
+ import { mkdirSync, existsSync, writeFileSync, readFileSync, readdirSync, rmSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { homedir } from 'node:os'
4
+ import { validatePlugin } from './plugin-validator'
5
+
6
+ const PLUGIN_DIR = join(homedir(), '.mipham', 'plugins')
7
+
8
+ export interface InstalledPlugin {
9
+ name: string
10
+ version: string
11
+ path: string
12
+ enabled: boolean
13
+ installedAt: string
14
+ }
15
+
16
+ export class PluginManager {
17
+ private plugins: InstalledPlugin[] = []
18
+ private pluginDir: string
19
+ private statePath: string
20
+
21
+ constructor(pluginDir?: string) {
22
+ this.pluginDir = pluginDir ?? PLUGIN_DIR
23
+ mkdirSync(this.pluginDir, { recursive: true })
24
+ this.statePath = join(this.pluginDir, 'state.json')
25
+ this.loadState()
26
+ }
27
+
28
+ install(sourcePath: string): { success: boolean; message: string } {
29
+ const validation = validatePlugin(sourcePath)
30
+ if (!validation.valid || !validation.manifest) {
31
+ return { success: false, message: validation.errors.join('; ') }
32
+ }
33
+
34
+ const destDir = join(this.pluginDir, validation.manifest.name)
35
+ if (existsSync(destDir)) {
36
+ return {
37
+ success: false,
38
+ message: `Plugin "${validation.manifest.name}" is already installed`,
39
+ }
40
+ }
41
+
42
+ // Copy plugin directory
43
+ mkdirSync(destDir, { recursive: true })
44
+ this.copyDir(sourcePath, destDir)
45
+
46
+ this.plugins.push({
47
+ name: validation.manifest.name,
48
+ version: validation.manifest.version,
49
+ path: destDir,
50
+ enabled: true,
51
+ installedAt: new Date().toISOString(),
52
+ })
53
+
54
+ this.saveState()
55
+ return {
56
+ success: true,
57
+ message: `Plugin "${validation.manifest.name}" v${validation.manifest.version} installed`,
58
+ }
59
+ }
60
+
61
+ list(): InstalledPlugin[] {
62
+ return [...this.plugins]
63
+ }
64
+
65
+ remove(name: string): boolean {
66
+ const plugin = this.plugins.find((p) => p.name === name)
67
+ if (!plugin) return false
68
+ try {
69
+ rmSync(plugin.path, { recursive: true, force: true })
70
+ } catch {
71
+ // Directory may already be gone — that's fine
72
+ }
73
+ this.plugins = this.plugins.filter((p) => p.name !== name)
74
+ this.saveState()
75
+ return true
76
+ }
77
+
78
+ enable(name: string): boolean {
79
+ const p = this.plugins.find((p) => p.name === name)
80
+ if (!p) return false
81
+ p.enabled = true
82
+ this.saveState()
83
+ return true
84
+ }
85
+
86
+ disable(name: string): boolean {
87
+ const p = this.plugins.find((p) => p.name === name)
88
+ if (!p) return false
89
+ p.enabled = false
90
+ this.saveState()
91
+ return true
92
+ }
93
+
94
+ getEnabled(): InstalledPlugin[] {
95
+ return this.plugins.filter((p) => p.enabled)
96
+ }
97
+
98
+ private copyDir(src: string, dest: string): void {
99
+ mkdirSync(dest, { recursive: true })
100
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
101
+ const srcPath = join(src, entry.name)
102
+ const destPath = join(dest, entry.name)
103
+ if (entry.isDirectory()) {
104
+ this.copyDir(srcPath, destPath)
105
+ } else {
106
+ const content = readFileSync(srcPath)
107
+ writeFileSync(destPath, content)
108
+ }
109
+ }
110
+ }
111
+
112
+ private loadState(): void {
113
+ try {
114
+ if (existsSync(this.statePath)) {
115
+ this.plugins = JSON.parse(readFileSync(this.statePath, 'utf-8'))
116
+ }
117
+ } catch {
118
+ this.plugins = []
119
+ }
120
+ }
121
+
122
+ private saveState(): void {
123
+ writeFileSync(this.statePath, JSON.stringify(this.plugins, null, 2), 'utf-8')
124
+ }
125
+ }
@@ -0,0 +1,41 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+
4
+ export interface PluginManifest {
5
+ name: string
6
+ version: string
7
+ miphamVersion?: string
8
+ hooks?: Record<string, unknown>
9
+ }
10
+
11
+ export function validatePlugin(dir: string): {
12
+ valid: boolean
13
+ errors: string[]
14
+ manifest?: PluginManifest
15
+ } {
16
+ const errors: string[] = []
17
+
18
+ try {
19
+ const raw = readFileSync(join(dir, 'plugin.json'), 'utf-8')
20
+ const manifest = JSON.parse(raw) as PluginManifest
21
+
22
+ if (!manifest.name || !/^[a-z0-9-]+$/.test(manifest.name)) {
23
+ errors.push('Invalid plugin name: must be lowercase alphanumeric with hyphens')
24
+ }
25
+ if (!manifest.version) {
26
+ errors.push('Missing required field: version')
27
+ }
28
+
29
+ // Check for suspicious hooks
30
+ if (manifest.hooks) {
31
+ const hooksStr = JSON.stringify(manifest.hooks)
32
+ if (hooksStr.includes('rm -rf') || hooksStr.includes('curl') || hooksStr.includes('eval')) {
33
+ errors.push('Suspicious hook commands detected — manual review required')
34
+ }
35
+ }
36
+
37
+ return { valid: errors.length === 0, errors, manifest }
38
+ } catch (err) {
39
+ return { valid: false, errors: [`Failed to read plugin.json: ${String(err)}`] }
40
+ }
41
+ }
@@ -59,7 +59,9 @@ export interface ToolContext {
59
59
  model: string
60
60
  skillsLoader?: import('../skills/loader').SkillsLoader
61
61
  registry?: import('../providers/registry').ProviderRegistry
62
+ toolRegistry?: Map<string, ToolDefinition>
62
63
  artifactServer?: import('../artifacts/server').ArtifactServer
64
+ agentRegistry?: import('../agent/agent-registry').AgentRegistry
63
65
  }
64
66
 
65
67
  // ── Artifact Types ──
@@ -131,6 +133,16 @@ export interface SkillDefinition {
131
133
  tools?: ToolDefinition[]
132
134
  hooks?: HookDefinition[]
133
135
  prompts?: Record<string, string>
136
+ /** Frontmatter: 'fork' means execute in isolated subagent, undefined means inline */
137
+ context?: string
138
+ /** Model override for fork execution */
139
+ model?: string
140
+ /** Tool whitelist for fork mode */
141
+ allowedTools?: string[]
142
+ /** When true, the skill is NOT shown in system-reminder for AI auto-triggering */
143
+ disableModelInvocation?: boolean
144
+ /** When true, users can invoke this skill directly via /<name> */
145
+ userInvocable?: boolean
134
146
  }
135
147
 
136
148
  // ── Hook Types ──
@@ -140,6 +152,25 @@ export type HookEvent =
140
152
  | 'SessionStart'
141
153
  | 'SessionEnd'
142
154
  | 'Notification'
155
+ | 'Stop'
156
+ | 'UserPromptSubmit'
157
+ | 'PreCompact'
158
+ | 'PostCompact'
159
+ | 'ConfigChange'
160
+
161
+ export type HookType = 'command' | 'http' | 'code' | 'mcp_tool'
162
+
163
+ export interface HookConfig {
164
+ type: HookType
165
+ command?: string
166
+ args?: string[]
167
+ url?: string
168
+ method?: 'GET' | 'POST'
169
+ headers?: Record<string, string>
170
+ mcpServer?: string
171
+ mcpTool?: string
172
+ continueOnBlock?: boolean
173
+ }
143
174
 
144
175
  export interface HookDefinition {
145
176
  event: HookEvent
@@ -153,12 +184,19 @@ export interface HookContext {
153
184
  toolInput?: Record<string, unknown>
154
185
  toolResult?: ToolResult
155
186
  sessionId: string
187
+ userPrompt?: string
188
+ configKey?: string
189
+ configValue?: unknown
156
190
  }
157
191
 
158
192
  export interface HookResult {
159
193
  allowed: boolean
160
194
  reason?: string
161
195
  modifiedInput?: Record<string, unknown>
196
+ decision?: 'allow' | 'block'
197
+ permissionDecision?: 'allow' | 'deny' | 'ask' | 'defer'
198
+ additionalContext?: string
199
+ updatedOutput?: string
162
200
  }
163
201
 
164
202
  // ── Instruction Types ──
@@ -172,7 +210,29 @@ export interface InstructionFile {
172
210
  }
173
211
 
174
212
  // ── Permission Types ──
175
- export type PermissionLevel = 'auto' | 'ask' | 'bypass'
213
+ /** Six explicit permission modes matching Claude Code's permission architecture */
214
+ export type PermissionMode =
215
+ | 'default'
216
+ | 'acceptEdits'
217
+ | 'plan'
218
+ | 'auto'
219
+ | 'dontAsk'
220
+ | 'bypassPermissions'
221
+
222
+ /** Backward-compatible alias: PermissionMode plus legacy 'ask' and 'bypass' */
223
+ export type PermissionLevel = PermissionMode | 'ask' | 'bypass'
224
+
225
+ export interface PermissionConfig {
226
+ mode: PermissionMode
227
+ allow: string[]
228
+ deny: string[]
229
+ }
230
+
231
+ export interface PermissionRuleEntry {
232
+ pattern: string // e.g., "Bash(git:*)"
233
+ level: 'allow' | 'deny' | 'ask'
234
+ compiled: RegExp
235
+ }
176
236
 
177
237
  export interface PermissionRule {
178
238
  toolName: string
@@ -0,0 +1,40 @@
1
+ import { SubAgent } from '../agent/sub-agent'
2
+ import type { ProviderRegistry } from '../providers/registry'
3
+ import type { ToolDefinition, SkillDefinition } from '../shared/index.ts'
4
+ import type { AgentDefinition } from '../agent/types'
5
+
6
+ /**
7
+ * Execute a skill in an isolated subagent context (context: fork).
8
+ *
9
+ * The skill's markdown body becomes the subagent's system prompt.
10
+ * The skill's allowed-tools become the subagent's tool whitelist.
11
+ * Results are returned to the AI as internal context (not shown directly to user).
12
+ */
13
+ export async function executeForkedSkill(
14
+ skill: SkillDefinition,
15
+ args: string,
16
+ registry: ProviderRegistry,
17
+ toolRegistry: Map<string, ToolDefinition>,
18
+ ): Promise<string> {
19
+ const agentDef: AgentDefinition = {
20
+ name: `skill:${skill.name}`,
21
+ description: skill.description,
22
+ systemPrompt: buildSkillSystemPrompt(skill),
23
+ tools: skill.allowedTools?.join(', '),
24
+ model: skill.model || 'inherit',
25
+ permissionMode: 'inherit',
26
+ background: false,
27
+ source: 'builtin',
28
+ }
29
+
30
+ const sub = new SubAgent(registry, toolRegistry)
31
+ const prompt = args
32
+ ? `Execute the "${skill.name}" skill with arguments: ${args}`
33
+ : `Execute the "${skill.name}" skill.`
34
+
35
+ return sub.execute(prompt, `skill:${skill.name}`, { agentDef })
36
+ }
37
+
38
+ function buildSkillSystemPrompt(skill: SkillDefinition): string {
39
+ return `You are executing the "${skill.name}" skill (v${skill.version}).\n\n${skill.description}\n\nFollow the skill instructions precisely and return results.`
40
+ }