@miphamai/cli 0.3.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 (74) hide show
  1. package/bin/mipham.ts +188 -0
  2. package/dist/mipham +0 -0
  3. package/package.json +9 -9
  4. package/skills/mipham/om-artifact.mipham-skill.md +69 -0
  5. package/skills/mipham/om-model-optimize.mipham-skill.md +9 -6
  6. package/skills/mipham/om-security.mipham-skill.md +11 -8
  7. package/skills/standard/doc-generator.SKILL.md +6 -1
  8. package/skills/standard/github-ops.SKILL.md +12 -7
  9. package/skills/standard/memory.SKILL.md +1 -0
  10. package/skills/standard/self-review.SKILL.md +1 -0
  11. package/skills/standard/superpower.SKILL.md +7 -7
  12. package/skills/standard/web-search.SKILL.md +3 -0
  13. package/src/agent/agent-context.ts +56 -0
  14. package/src/agent/agent-registry.ts +134 -0
  15. package/src/agent/sub-agent.ts +73 -89
  16. package/src/agent/types.ts +39 -0
  17. package/src/agent-view/agent-view-manager.ts +217 -0
  18. package/src/agent-view/dashboard.tsx +218 -0
  19. package/src/agent-view/session-peek.tsx +72 -0
  20. package/src/agent-view/session-row.tsx +70 -0
  21. package/src/artifacts/manifest.ts +101 -0
  22. package/src/artifacts/server.ts +374 -0
  23. package/src/artifacts/versioning.ts +127 -0
  24. package/src/core/context-compact.ts +50 -0
  25. package/src/core/context-drain.ts +54 -0
  26. package/src/core/context-microcompact.ts +132 -0
  27. package/src/core/context-snip.ts +74 -0
  28. package/src/core/context-token.ts +108 -0
  29. package/src/core/context.ts +169 -0
  30. package/src/core/engine.ts +207 -58
  31. package/src/core/hooks-config.ts +52 -0
  32. package/src/core/hooks-executor.ts +113 -0
  33. package/src/core/hooks.ts +90 -30
  34. package/src/core/instructions.ts +12 -1
  35. package/src/core/memory/memory-loader.ts +23 -0
  36. package/src/core/memory/memory-manager.ts +192 -0
  37. package/src/core/memory/memory-writer.ts +72 -0
  38. package/src/core/permission-config.ts +34 -0
  39. package/src/core/permission-rules.ts +66 -0
  40. package/src/core/permission.ts +224 -34
  41. package/src/index.tsx +30 -2
  42. package/src/mcp/transport.ts +42 -5
  43. package/src/plugin/plugin-loader.ts +36 -0
  44. package/src/plugin/plugin-manager.ts +125 -0
  45. package/src/plugin/plugin-validator.ts +41 -0
  46. package/src/providers/fetch-utils.ts +1 -3
  47. package/src/providers/openai-compat.ts +2 -11
  48. package/src/shared/constants.ts +8 -1
  49. package/src/shared/types.ts +82 -2
  50. package/src/skills/fork-executor.ts +40 -0
  51. package/src/skills/loader.ts +48 -2
  52. package/src/tools/agent/agent.ts +20 -11
  53. package/src/tools/agent/skill.ts +32 -2
  54. package/src/tools/artifact/artifact.ts +144 -0
  55. package/src/tools/computer/app-launcher.ts +39 -0
  56. package/src/tools/computer/browser.ts +48 -0
  57. package/src/tools/computer/computer-use.ts +88 -0
  58. package/src/tools/computer/playwright.d.ts +7 -0
  59. package/src/tools/computer/screenshot.ts +57 -0
  60. package/src/tools/index.ts +6 -0
  61. package/src/ui/app.tsx +20 -7
  62. package/src/ui/chat.tsx +9 -5
  63. package/src/ui/commands.ts +185 -40
  64. package/src/ui/input.tsx +203 -27
  65. package/src/ui/picker.tsx +2 -5
  66. package/src/ui/vim-motions.ts +96 -0
  67. package/src/workflow/budget.ts +33 -0
  68. package/src/workflow/journal.ts +105 -0
  69. package/src/workflow/primitives/agent.ts +51 -0
  70. package/src/workflow/primitives/parallel.ts +8 -0
  71. package/src/workflow/primitives/phase.ts +19 -0
  72. package/src/workflow/primitives/pipeline.ts +24 -0
  73. package/src/workflow/runtime.ts +87 -0
  74. package/src/workflow/sandbox.ts +75 -0
@@ -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
+ }
@@ -15,6 +15,8 @@ import { webFetchTool } from './network/web-fetch'
15
15
  import { webSearchTool } from './network/web-search'
16
16
  import { configTool } from './system/config'
17
17
  import { mcpTool } from './system/mcp'
18
+ import { artifactTool } from './artifact/artifact'
19
+ import { computerUseTool } from './computer/computer-use'
18
20
 
19
21
  /**
20
22
  * Validate tool parameters against the tool's JSON Schema definition.
@@ -116,6 +118,10 @@ export function createToolRegistry(): Map<string, ToolDefinition> {
116
118
  // System tools
117
119
  withValidation(configTool),
118
120
  withValidation(mcpTool),
121
+ // Artifact tools
122
+ withValidation(artifactTool),
123
+ // Computer Use tools
124
+ withValidation(computerUseTool),
119
125
  ]
120
126
 
121
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'
@@ -58,7 +59,7 @@ export function App({
58
59
  config,
59
60
  initialProvider,
60
61
  initialModel,
61
- lang,
62
+ lang: _lang,
62
63
  skillsLoader,
63
64
  }: AppProps) {
64
65
  const [messages, setMessages] = useState<ChatMessage[]>([])
@@ -78,13 +79,24 @@ export function App({
78
79
 
79
80
  // Agent elapsed timer
80
81
  React.useEffect(() => {
81
- if (!agentProgress) { setAgentElapsed(0); return }
82
+ if (!agentProgress) {
83
+ setAgentElapsed(0)
84
+ return
85
+ }
82
86
  const interval = setInterval(() => {
83
87
  setAgentElapsed(Math.floor((Date.now() - agentProgress.startTime) / 1000))
84
88
  }, 1000)
85
89
  return () => clearInterval(interval)
86
90
  }, [agentProgress])
87
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
+
88
100
  const mkCtx = useCallback(
89
101
  (): CommandContext => ({
90
102
  engine,
@@ -216,8 +228,10 @@ export function App({
216
228
  if (isAgent) {
217
229
  setAgentProgress({
218
230
  name: (chunk.toolUse.input.subagent_type as string) || 'General-purpose',
219
- description: (chunk.toolUse.input.description as string) ||
220
- (chunk.toolUse.input.prompt as string) || '',
231
+ description:
232
+ (chunk.toolUse.input.description as string) ||
233
+ (chunk.toolUse.input.prompt as string) ||
234
+ '',
221
235
  startTime: Date.now(),
222
236
  })
223
237
  }
@@ -379,8 +393,7 @@ export function App({
379
393
  {agentProgress && (
380
394
  <Box flexDirection="column" marginY={1}>
381
395
  <Text color="cyan" bold>
382
- ⏺ {agentProgress.name}{' '}
383
- <Text color="yellow">{agentElapsed}s</Text>
396
+ ⏺ {agentProgress.name} <Text color="yellow">{agentElapsed}s</Text>
384
397
  </Text>
385
398
  <Text dimColor>
386
399
  {agentProgress.description.length > 100
package/src/ui/chat.tsx CHANGED
@@ -39,8 +39,8 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
39
39
  </Box>
40
40
  <Box marginTop={1}>
41
41
  <Text dimColor>
42
- Tip: Use <Text color="yellow">/clear</Text> to start fresh when switching topics
43
- and free up context
42
+ Tip: Use <Text color="yellow">/clear</Text> to start fresh when switching topics and
43
+ free up context
44
44
  </Text>
45
45
  </Box>
46
46
  </Box>
@@ -50,8 +50,7 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
50
50
  {msg.toolMeta ? (
51
51
  <Box flexDirection="column">
52
52
  <Text color="yellow">
53
- {msg.toolMeta.collapsed ? '⏺' : '⏺ ▼'}{' '}
54
- {msg.toolMeta.name}
53
+ {msg.toolMeta.collapsed ? '⏺' : '⏺ ▼'} {msg.toolMeta.name}
55
54
  </Text>
56
55
  <Text dimColor>{msg.content}</Text>
57
56
  </Box>
@@ -61,7 +60,12 @@ export function ChatPanel({ messages, focusMode }: ChatPanelProps) {
61
60
  bold
62
61
  color={msg.role === 'user' ? 'green' : msg.role === 'system' ? 'yellow' : 'blue'}
63
62
  >
64
- {msg.role === 'user' ? '▸ You' : msg.role === 'assistant' ? 'Mipham Code' : '⚠ System'}:
63
+ {msg.role === 'user'
64
+ ? '▸ You'
65
+ : msg.role === 'assistant'
66
+ ? 'Mipham Code'
67
+ : '⚠ System'}
68
+ :
65
69
  </Text>
66
70
  <Text>{msg.content}</Text>
67
71
  </>
@@ -8,14 +8,7 @@ import type { QueryEngine } from '../core/engine'
8
8
  import type { MiphamConfig } from '../shared/index.ts'
9
9
  import type { SkillsLoader } from '../skills/loader'
10
10
  import { McpClient } from '../mcp/client'
11
- import {
12
- PACKAGE_NAME,
13
- NPM_INSTALL_COMMAND,
14
- NPM_UPDATE_COMMAND,
15
- NPM_URL,
16
- PRODUCT_URL_INTERNATIONAL,
17
- PRODUCT_URL_CHINA,
18
- } from '@mipham/shared'
11
+ import { NPM_INSTALL_COMMAND, NPM_UPDATE_COMMAND } from '@mipham/shared'
19
12
 
20
13
  export interface CommandContext {
21
14
  engine: QueryEngine
@@ -133,7 +126,10 @@ const helpCmd: CommandHandler = (_ctx) => ({
133
126
  /login Show API key status
134
127
  /logout Clear credentials guide
135
128
  /feedback Send feedback
136
- /agents Agent management
129
+
130
+ ── Agents ──────────────────────────
131
+ /agents Agent view dashboard
132
+ /bg <prompt> Run a background agent task
137
133
 
138
134
  Type /exit or Esc to quit.
139
135
  `,
@@ -421,12 +417,12 @@ const renameCmd: CommandHandler = (ctx, args) => {
421
417
  const goalCmd: CommandHandler = (ctx, args) => {
422
418
  const goal = args.join(' ')
423
419
  if (!goal.trim()) {
424
- const c = ctx.engine.getContext()
425
420
  return {
426
421
  content: `Usage: /goal <statement>\n\nSet a session-level completion condition. Mipham Code will track progress toward this goal.\n\nExample: /goal Fix all TypeScript errors and make tests pass`,
427
422
  }
428
423
  }
429
424
  ctx.setGoal(goal.trim())
425
+ ctx.engine.setGoal(goal.trim())
430
426
  return {
431
427
  content: `✓ Goal set: "${goal.trim()}"\n\nUse /status to view progress. Type /goal without arguments to clear.`,
432
428
  }
@@ -867,7 +863,7 @@ const doctorCmd: CommandHandler = async (ctx) => {
867
863
  // ═══════════════════════════════════════════════════════════════
868
864
 
869
865
  const exportCmd: CommandHandler = async (ctx) => {
870
- const { writeFileSync, existsSync } = await import('node:fs')
866
+ const { writeFileSync } = await import('node:fs')
871
867
  const { join } = await import('node:path')
872
868
 
873
869
  const cwd = process.cwd()
@@ -2086,47 +2082,192 @@ const feedbackCmd: CommandHandler = (ctx, args) => {
2086
2082
  // Phase 4 — Agent Management
2087
2083
  // ═══════════════════════════════════════════════════════════════
2088
2084
 
2089
- const agentsCmd: CommandHandler = () => ({
2090
- content: `── Agent System ──
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
2091
2092
 
2092
- Mipham Code includes 4 agent-type tools for structured workflows:
2093
+ if (total === 0) {
2094
+ return {
2095
+ content: `── Agent View ──
2093
2096
 
2094
- ┌──────────┬──────────────────────────────────────┬──────────┬──────────────────────────┐
2095
- │ Agent │ Category │ Permission │ Parameters │
2096
- ├──────────┼──────────┼────────────┼────────────────────────────────┤
2097
- │ Agent │ agent │ ask │ description*, prompt*, │
2098
- │ │ │ │ subagent_type (optional) │
2099
- │ Skill │ agent │ auto │ skill*, args (optional) │
2100
- │ Plan │ agent │ auto │ (no required params) │
2101
- │ Memory │ agent │ auto │ action* (read|write|list), │
2102
- │ │ │ │ name, content │
2103
- └──────────┴──────────┴────────────┴────────────────────────────────┘
2097
+ No background agents running.
2104
2098
 
2105
- ── Agent Descriptions ──
2099
+ Use /bg <prompt> to spawn a background agent session.
2106
2100
 
2107
- Agent Launch a sub-agent for complex, multi-step tasks.
2108
- Types: general-purpose, Explore, Plan, code-reviewer, etc.
2101
+ Example:
2102
+ /bg Fix all TypeScript errors in the project
2103
+ /bg Review the auth module for security issues
2109
2104
 
2110
- Skill Invoke a named skill (11 built-in, custom via .SKILL.md).
2111
- Skills extend AI capabilities with specialized instructions.
2105
+ Or use the Agent tool in a conversation to launch a sub-agent.`,
2106
+ }
2107
+ }
2112
2108
 
2113
- Plan Enter plan mode read-only analysis and design.
2114
- Use before complex changes. Exit with /no-plan.
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
+ ]
2115
2120
 
2116
- Memory Persistent memory read/write across sessions.
2117
- Stored in ~/.mipham/memory/ as markdown files.
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
+ }
2118
2139
 
2119
- ── Architecture ──
2140
+ if (total > 10) {
2141
+ lines.push(` ... and ${total - 10} more`)
2142
+ }
2120
2143
 
2121
- User QueryEngine Agent Tool Sub-agent Tool Results → User
2144
+ lines.push('', 'Run `mipham agents` for the full interactive dashboard with j/k navigation.')
2122
2145
 
2123
- Agent tools are dispatched by the AI during conversations.
2124
- The AI decides when to use them — you can also request them directly.
2146
+ return { content: lines.join('\n') }
2147
+ }
2125
2148
 
2126
- Full multi-agent orchestration (Workflow tool) is in the M3 milestone.
2149
+ const bgCmd: CommandHandler = (ctx, args) => {
2150
+ const prompt = args.join(' ')
2151
+ if (!prompt.trim()) {
2152
+ return {
2153
+ content: `Usage: /bg <prompt>
2127
2154
 
2128
- Use /tools to see all 16 tools, including the 4 agent tools.`,
2129
- })
2155
+ Spawn a background agent to work on a task while you continue chatting.
2156
+
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
2161
+
2162
+ Background agents appear in the Agent View dashboard (/agents).`,
2163
+ }
2164
+ }
2165
+
2166
+ const agentViewManager = ctx.engine.getAgentViewManager?.()
2167
+ if (!agentViewManager) {
2168
+ return { content: 'Agent View manager is initializing...' }
2169
+ }
2170
+
2171
+ const session = agentViewManager.create(prompt, prompt, {
2172
+ provider: ctx.providerId,
2173
+ model: ctx.modelId,
2174
+ })
2175
+
2176
+ agentViewManager.addMessage(session.id, { role: 'user', content: prompt })
2177
+ agentViewManager.updateStatus(session.id, 'working')
2178
+
2179
+ return {
2180
+ content: `✓ Background agent spawned: ${session.id}
2181
+
2182
+ Task: ${session.task.slice(0, 80)}
2183
+ Provider: ${session.provider} / ${session.model}
2184
+ Status: working
2185
+
2186
+ Use /agents to view all background sessions.
2187
+ Run \`mipham agents\` for the interactive dashboard.`,
2188
+ }
2189
+ }
2190
+
2191
+ // ═══════════════════════════════════════════════════════════════
2192
+ // Artifacts
2193
+ // ═══════════════════════════════════════════════════════════════
2194
+
2195
+ async function openBrowser(url: string): Promise<void> {
2196
+ const cmd =
2197
+ process.platform === 'darwin'
2198
+ ? `open "${url}"`
2199
+ : process.platform === 'win32'
2200
+ ? `start "" "${url}"`
2201
+ : `xdg-open "${url}"`
2202
+ const { exec } = await import('node:child_process')
2203
+ exec(cmd, () => {
2204
+ /* fire-and-forget */
2205
+ })
2206
+ }
2207
+
2208
+ const artifactOpenCmd: CommandHandler = async (ctx, args) => {
2209
+ const name = args[0]
2210
+ if (!name) {
2211
+ return { content: 'Usage: /artifact open <name>\nExample: /artifact open dashboard' }
2212
+ }
2213
+
2214
+ const sessionId = 'session-1' // default session
2215
+ const port = 9876
2216
+ const ext = name.endsWith('.svg') ? '' : '.html'
2217
+ const url = `http://localhost:${port}/${sessionId}/${name}${ext}`
2218
+
2219
+ try {
2220
+ await openBrowser(url)
2221
+ return { content: `✓ Opening artifact "${name}" in browser...\n ${url}` }
2222
+ } catch (err) {
2223
+ return { content: `✗ Failed to open browser: ${String(err)}\n URL: ${url}` }
2224
+ }
2225
+ }
2226
+
2227
+ const artifactListCmd: CommandHandler = async (_ctx, _args) => {
2228
+ const { getSessionArtifacts } = await import('../artifacts/manifest')
2229
+ const { join } = await import('node:path')
2230
+ const { ARTIFACTS_DIR } = await import('../shared/constants')
2231
+
2232
+ const dir = join(process.cwd(), ARTIFACTS_DIR)
2233
+ const entries = getSessionArtifacts(dir, 'session-1')
2234
+
2235
+ if (entries.length === 0) {
2236
+ return {
2237
+ content: 'No artifacts created yet. Ask the AI to generate one with the Artifact tool.',
2238
+ }
2239
+ }
2240
+
2241
+ const lines = ['── Artifacts ──', '']
2242
+ for (const e of entries) {
2243
+ const ver = e.versions && e.versions.length > 1 ? ` v${e.versions.length}` : ''
2244
+ lines.push(
2245
+ ` ${e.name.padEnd(24)} ${(e.type + ver).padEnd(8)} ${(e.size / 1024).toFixed(1).padStart(8)}KB ${e.createdAt.slice(0, 16).replace('T', ' ')}`,
2246
+ )
2247
+ }
2248
+ lines.push('', ` ${entries.length} artifact(s) — /artifact open <name> to view`)
2249
+ lines.push(` Gallery: http://localhost:9876`)
2250
+
2251
+ return { content: lines.join('\n') }
2252
+ }
2253
+
2254
+ const artifactServerCmd: CommandHandler = async (_ctx, _args) => {
2255
+ const url = 'http://localhost:9876'
2256
+ return {
2257
+ content: `── Artifact Server ──\n\n URL: ${url}\n Port: 9876\n\nArtifacts are served locally. No external network access.`,
2258
+ }
2259
+ }
2260
+
2261
+ const artifactBaseCmd: CommandHandler = (ctx, args) => {
2262
+ const sub = args[0]
2263
+ if (sub === 'open') return artifactOpenCmd(ctx, args.slice(1))
2264
+ if (sub === 'list') return artifactListCmd(ctx, [])
2265
+ if (sub === 'server') return artifactServerCmd(ctx, [])
2266
+ return {
2267
+ content:
2268
+ 'Usage: /artifact <open|list|server> [name]\n\n /artifact open <name> Open artifact in browser\n /artifact list List all artifacts\n /artifact server Show server status',
2269
+ }
2270
+ }
2130
2271
 
2131
2272
  // ═══════════════════════════════════════════════════════════════
2132
2273
  // Remaining low-priority stubs (backend not yet available)
@@ -2213,6 +2354,10 @@ registry.set('/login', loginCmd)
2213
2354
  registry.set('/logout', logoutCmd)
2214
2355
  registry.set('/feedback', feedbackCmd)
2215
2356
  registry.set('/agents', agentsCmd)
2357
+ registry.set('/bg', bgCmd)
2358
+
2359
+ // Artifacts
2360
+ registry.set('/artifact', artifactBaseCmd)
2216
2361
 
2217
2362
  // Lower-priority semi-stubs (WIP, backend pending)
2218
2363
  registry.set('/branch', branchCmd)