@miphamai/cli 0.3.0 → 0.4.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 +2 -0
- package/dist/mipham +0 -0
- package/package.json +9 -9
- package/skills/mipham/om-artifact.mipham-skill.md +69 -0
- package/skills/mipham/om-model-optimize.mipham-skill.md +9 -6
- package/skills/mipham/om-security.mipham-skill.md +11 -8
- package/skills/standard/doc-generator.SKILL.md +6 -1
- package/skills/standard/github-ops.SKILL.md +12 -7
- package/skills/standard/memory.SKILL.md +1 -0
- package/skills/standard/self-review.SKILL.md +1 -0
- package/skills/standard/superpower.SKILL.md +7 -7
- package/skills/standard/web-search.SKILL.md +3 -0
- package/src/agent/sub-agent.ts +0 -1
- package/src/artifacts/manifest.ts +101 -0
- package/src/artifacts/server.ts +343 -0
- package/src/core/engine.ts +63 -55
- package/src/core/instructions.ts +1 -1
- package/src/core/permission.ts +7 -2
- package/src/index.tsx +10 -2
- package/src/mcp/transport.ts +42 -5
- package/src/providers/fetch-utils.ts +1 -3
- package/src/providers/openai-compat.ts +2 -11
- package/src/shared/constants.ts +8 -1
- package/src/shared/types.ts +21 -1
- package/src/skills/loader.ts +2 -2
- package/src/tools/artifact/artifact.ts +144 -0
- package/src/tools/index.ts +3 -0
- package/src/ui/app.tsx +10 -6
- package/src/ui/chat.tsx +9 -5
- package/src/ui/commands.ts +86 -10
- package/src/ui/input.tsx +80 -19
- package/src/ui/picker.tsx +2 -5
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ProviderConfig,
|
|
3
|
-
ModelInfo,
|
|
4
|
-
Message,
|
|
5
|
-
StreamChunk,
|
|
6
|
-
ContentBlock,
|
|
7
|
-
} from '../shared/index.ts'
|
|
1
|
+
import type { ProviderConfig, ModelInfo, Message, StreamChunk } from '../shared/index.ts'
|
|
8
2
|
import type { ProviderInstance, ChatRequest } from './registry'
|
|
9
3
|
import { fetchWithRetry } from './fetch-utils'
|
|
10
4
|
|
|
@@ -49,10 +43,7 @@ export class OpenAICompatProvider implements ProviderInstance {
|
|
|
49
43
|
let buffer = ''
|
|
50
44
|
|
|
51
45
|
// Track incremental tool calls across streaming deltas
|
|
52
|
-
const pendingToolCalls = new Map<
|
|
53
|
-
number,
|
|
54
|
-
{ id: string; name: string; arguments: string }
|
|
55
|
-
>()
|
|
46
|
+
const pendingToolCalls = new Map<number, { id: string; name: string; arguments: string }>()
|
|
56
47
|
|
|
57
48
|
while (true) {
|
|
58
49
|
const { done, value } = await reader.read()
|
package/src/shared/constants.ts
CHANGED
|
@@ -361,8 +361,15 @@ export const PROTOCOL_LABELS: Record<string, string> = {
|
|
|
361
361
|
custom: 'Custom Protocol',
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
export const TOOL_CATEGORIES = ['file', 'exec', 'agent', 'network', 'system'] as const
|
|
364
|
+
export const TOOL_CATEGORIES = ['file', 'exec', 'agent', 'network', 'system', 'artifact'] as const
|
|
365
365
|
export const CONFIG_FILE_NAME = 'config.yml'
|
|
366
366
|
export const MIPHAM_DIR = '.mipham'
|
|
367
367
|
export const USER_CONFIG_DIR = '.mipham'
|
|
368
368
|
export const MEMORY_DIR = 'memory'
|
|
369
|
+
|
|
370
|
+
// ── Artifact Constants ──
|
|
371
|
+
export const ARTIFACTS_DIR = 'artifacts'
|
|
372
|
+
export const ARTIFACT_PORT = 9876
|
|
373
|
+
export const ARTIFACT_MAX_PORT_TRIES = 10
|
|
374
|
+
export const ARTIFACT_MAX_SIZE = 5 * 1024 * 1024 // 5MB
|
|
375
|
+
export const ARTIFACT_ALLOWED_EXTENSIONS = ['.html', '.svg']
|
package/src/shared/types.ts
CHANGED
|
@@ -50,7 +50,7 @@ export interface Message {
|
|
|
50
50
|
|
|
51
51
|
// ── Tool Types ──
|
|
52
52
|
export type ToolPermission = 'auto' | 'ask' | 'bypass'
|
|
53
|
-
export type ToolCategory = 'file' | 'exec' | 'agent' | 'network' | 'system'
|
|
53
|
+
export type ToolCategory = 'file' | 'exec' | 'agent' | 'network' | 'system' | 'artifact'
|
|
54
54
|
|
|
55
55
|
export interface ToolContext {
|
|
56
56
|
cwd: string
|
|
@@ -59,6 +59,26 @@ export interface ToolContext {
|
|
|
59
59
|
model: string
|
|
60
60
|
skillsLoader?: import('../skills/loader').SkillsLoader
|
|
61
61
|
registry?: import('../providers/registry').ProviderRegistry
|
|
62
|
+
artifactServer?: import('../artifacts/server').ArtifactServer
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Artifact Types ──
|
|
66
|
+
export interface ArtifactEntry {
|
|
67
|
+
name: string
|
|
68
|
+
path: string
|
|
69
|
+
url: string
|
|
70
|
+
size: number
|
|
71
|
+
type: 'html' | 'svg'
|
|
72
|
+
createdAt: string
|
|
73
|
+
sessionId: string
|
|
74
|
+
versions?: string[] // version tags e.g. ['v1', 'v2']
|
|
75
|
+
versionCount?: number
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ArtifactManifest {
|
|
79
|
+
version: 1
|
|
80
|
+
artifacts: ArtifactEntry[]
|
|
81
|
+
port?: number
|
|
62
82
|
}
|
|
63
83
|
|
|
64
84
|
export interface ToolResult {
|
package/src/skills/loader.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
-
import { join
|
|
2
|
+
import { join } from 'node:path'
|
|
3
3
|
import { parse as parseYaml } from 'yaml'
|
|
4
4
|
import type { SkillDefinition } from '../shared/index.ts'
|
|
5
5
|
|
|
@@ -84,7 +84,7 @@ export class SkillsLoader {
|
|
|
84
84
|
private tryLoad(path: string, type: 'standard' | 'mipham'): void {
|
|
85
85
|
try {
|
|
86
86
|
const raw = readFileSync(path, 'utf-8')
|
|
87
|
-
const { data
|
|
87
|
+
const { data } = parseFrontmatter(raw)
|
|
88
88
|
|
|
89
89
|
const skill: SkillDefinition = {
|
|
90
90
|
name: (data.name as string) || this.nameFromPath(path),
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { ToolDefinition } from '../../shared/index.ts'
|
|
4
|
+
import { ARTIFACTS_DIR, ARTIFACT_MAX_SIZE } from '../../shared/constants'
|
|
5
|
+
import { addToManifest, readManifest, archiveVersion } from '../../artifacts/manifest'
|
|
6
|
+
|
|
7
|
+
const NAME_PATTERN = /^[a-z0-9][a-z0-9-]*[a-z0-9]$/
|
|
8
|
+
|
|
9
|
+
export const artifactTool: ToolDefinition = {
|
|
10
|
+
name: 'Artifact',
|
|
11
|
+
description:
|
|
12
|
+
'Create an interactive HTML or SVG artifact that opens in the browser. Use for dashboards, visualizations, reports, and interactive demos. Content must be self-contained — no external CDN references.',
|
|
13
|
+
category: 'artifact',
|
|
14
|
+
permission: 'ask',
|
|
15
|
+
parameters: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
name: {
|
|
19
|
+
type: 'string',
|
|
20
|
+
description: 'Short kebab-case name (e.g. "user-dashboard", "pipeline-diagram")',
|
|
21
|
+
},
|
|
22
|
+
type: {
|
|
23
|
+
type: 'string',
|
|
24
|
+
enum: ['html', 'svg'],
|
|
25
|
+
description: 'Artifact type',
|
|
26
|
+
},
|
|
27
|
+
content: {
|
|
28
|
+
type: 'string',
|
|
29
|
+
description: 'Full HTML or SVG content (self-contained, inline CSS/JS only)',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
required: ['name', 'type', 'content'],
|
|
33
|
+
},
|
|
34
|
+
async execute(params, ctx) {
|
|
35
|
+
const name = params.name as string
|
|
36
|
+
const type = params.type as string
|
|
37
|
+
const content = params.content as string
|
|
38
|
+
|
|
39
|
+
// Validate name
|
|
40
|
+
if (!NAME_PATTERN.test(name) || name.length < 3) {
|
|
41
|
+
return {
|
|
42
|
+
success: false,
|
|
43
|
+
content: '',
|
|
44
|
+
error:
|
|
45
|
+
`Invalid artifact name "${name}". Use kebab-case (letters, numbers, hyphens), ` +
|
|
46
|
+
`at least 3 characters, e.g. "my-dashboard".`,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Validate size
|
|
51
|
+
const size = Buffer.byteLength(content, 'utf-8')
|
|
52
|
+
if (size > ARTIFACT_MAX_SIZE) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
content: '',
|
|
56
|
+
error: `Content size ${(size / 1_048_576).toFixed(1)}MB exceeds ${ARTIFACT_MAX_SIZE / 1_048_576}MB limit.`,
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Determine output paths
|
|
61
|
+
const baseDir = join(ctx.cwd, ARTIFACTS_DIR)
|
|
62
|
+
const sessionDir = join(baseDir, ctx.sessionId)
|
|
63
|
+
mkdirSync(sessionDir, { recursive: true })
|
|
64
|
+
|
|
65
|
+
const ext = type === 'svg' ? '.svg' : '.html'
|
|
66
|
+
const filename = `${name}${ext}`
|
|
67
|
+
const filepath = join(sessionDir, filename)
|
|
68
|
+
|
|
69
|
+
// Start server if not running
|
|
70
|
+
let port: number | undefined
|
|
71
|
+
if (ctx.artifactServer && !ctx.artifactServer.isRunning()) {
|
|
72
|
+
port = await ctx.artifactServer.start()
|
|
73
|
+
} else if (ctx.artifactServer) {
|
|
74
|
+
port = ctx.artifactServer.getPort()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Archive previous version if this artifact already exists
|
|
78
|
+
let archivedVersion: string | undefined
|
|
79
|
+
const isUpdate = existsSync(filepath)
|
|
80
|
+
if (isUpdate) {
|
|
81
|
+
const manifest = readManifest(baseDir)
|
|
82
|
+
const existing = manifest.artifacts.find(
|
|
83
|
+
(a) => a.name === name && a.sessionId === ctx.sessionId,
|
|
84
|
+
)
|
|
85
|
+
if (existing) {
|
|
86
|
+
archivedVersion = archiveVersion(baseDir, existing)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Write artifact
|
|
91
|
+
writeFileSync(filepath, content, 'utf-8')
|
|
92
|
+
|
|
93
|
+
// Build URL and manifest entry
|
|
94
|
+
const url = port
|
|
95
|
+
? `http://localhost:${port}/${ctx.sessionId}/${filename}`
|
|
96
|
+
: `file://${filepath}`
|
|
97
|
+
|
|
98
|
+
// Preserve existing version count
|
|
99
|
+
const manifestPre = readManifest(baseDir)
|
|
100
|
+
const prev = manifestPre.artifacts.find((a) => a.name === name && a.sessionId === ctx.sessionId)
|
|
101
|
+
const versionCount = prev?.versionCount || (isUpdate ? 1 : undefined)
|
|
102
|
+
|
|
103
|
+
addToManifest(
|
|
104
|
+
baseDir,
|
|
105
|
+
{
|
|
106
|
+
name,
|
|
107
|
+
path: filepath,
|
|
108
|
+
url,
|
|
109
|
+
size,
|
|
110
|
+
type: type as 'html' | 'svg',
|
|
111
|
+
createdAt: new Date().toISOString(),
|
|
112
|
+
sessionId: ctx.sessionId,
|
|
113
|
+
versions: prev?.versions,
|
|
114
|
+
versionCount,
|
|
115
|
+
},
|
|
116
|
+
port,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
// Notify SSE clients to refresh Gallery
|
|
120
|
+
if (ctx.artifactServer) {
|
|
121
|
+
ctx.artifactServer.notifyReload()
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const galleryUrl = port ? `http://localhost:${port}` : undefined
|
|
125
|
+
const versionLine = archivedVersion ? ` Prev archived as: ${archivedVersion}` : ''
|
|
126
|
+
const galleryLine = galleryUrl ? `Gallery: ${galleryUrl}` : ''
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
content: [
|
|
131
|
+
`✓ Artifact "${name}" saved${isUpdate ? ' (updated)' : ''}.`,
|
|
132
|
+
` URL: ${url}`,
|
|
133
|
+
` Size: ${size.toLocaleString()} bytes`,
|
|
134
|
+
versionLine,
|
|
135
|
+
galleryLine,
|
|
136
|
+
'',
|
|
137
|
+
`Open in browser: /artifact open ${name}`,
|
|
138
|
+
`List all: /artifact list`,
|
|
139
|
+
]
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join('\n'),
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -15,6 +15,7 @@ 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'
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Validate tool parameters against the tool's JSON Schema definition.
|
|
@@ -116,6 +117,8 @@ export function createToolRegistry(): Map<string, ToolDefinition> {
|
|
|
116
117
|
// System tools
|
|
117
118
|
withValidation(configTool),
|
|
118
119
|
withValidation(mcpTool),
|
|
120
|
+
// Artifact tools
|
|
121
|
+
withValidation(artifactTool),
|
|
119
122
|
]
|
|
120
123
|
|
|
121
124
|
const map = new Map<string, ToolDefinition>()
|
package/src/ui/app.tsx
CHANGED
|
@@ -58,7 +58,7 @@ export function App({
|
|
|
58
58
|
config,
|
|
59
59
|
initialProvider,
|
|
60
60
|
initialModel,
|
|
61
|
-
lang,
|
|
61
|
+
lang: _lang,
|
|
62
62
|
skillsLoader,
|
|
63
63
|
}: AppProps) {
|
|
64
64
|
const [messages, setMessages] = useState<ChatMessage[]>([])
|
|
@@ -78,7 +78,10 @@ export function App({
|
|
|
78
78
|
|
|
79
79
|
// Agent elapsed timer
|
|
80
80
|
React.useEffect(() => {
|
|
81
|
-
if (!agentProgress) {
|
|
81
|
+
if (!agentProgress) {
|
|
82
|
+
setAgentElapsed(0)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
82
85
|
const interval = setInterval(() => {
|
|
83
86
|
setAgentElapsed(Math.floor((Date.now() - agentProgress.startTime) / 1000))
|
|
84
87
|
}, 1000)
|
|
@@ -216,8 +219,10 @@ export function App({
|
|
|
216
219
|
if (isAgent) {
|
|
217
220
|
setAgentProgress({
|
|
218
221
|
name: (chunk.toolUse.input.subagent_type as string) || 'General-purpose',
|
|
219
|
-
description:
|
|
220
|
-
(chunk.toolUse.input.
|
|
222
|
+
description:
|
|
223
|
+
(chunk.toolUse.input.description as string) ||
|
|
224
|
+
(chunk.toolUse.input.prompt as string) ||
|
|
225
|
+
'',
|
|
221
226
|
startTime: Date.now(),
|
|
222
227
|
})
|
|
223
228
|
}
|
|
@@ -379,8 +384,7 @@ export function App({
|
|
|
379
384
|
{agentProgress && (
|
|
380
385
|
<Box flexDirection="column" marginY={1}>
|
|
381
386
|
<Text color="cyan" bold>
|
|
382
|
-
⏺ {agentProgress.name}{
|
|
383
|
-
<Text color="yellow">{agentElapsed}s</Text>
|
|
387
|
+
⏺ {agentProgress.name} <Text color="yellow">{agentElapsed}s</Text>
|
|
384
388
|
</Text>
|
|
385
389
|
<Text dimColor>
|
|
386
390
|
{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
|
-
|
|
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'
|
|
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
|
</>
|
package/src/ui/commands.ts
CHANGED
|
@@ -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
|
|
@@ -421,7 +414,6 @@ const renameCmd: CommandHandler = (ctx, args) => {
|
|
|
421
414
|
const goalCmd: CommandHandler = (ctx, args) => {
|
|
422
415
|
const goal = args.join(' ')
|
|
423
416
|
if (!goal.trim()) {
|
|
424
|
-
const c = ctx.engine.getContext()
|
|
425
417
|
return {
|
|
426
418
|
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
419
|
}
|
|
@@ -867,7 +859,7 @@ const doctorCmd: CommandHandler = async (ctx) => {
|
|
|
867
859
|
// ═══════════════════════════════════════════════════════════════
|
|
868
860
|
|
|
869
861
|
const exportCmd: CommandHandler = async (ctx) => {
|
|
870
|
-
const { writeFileSync
|
|
862
|
+
const { writeFileSync } = await import('node:fs')
|
|
871
863
|
const { join } = await import('node:path')
|
|
872
864
|
|
|
873
865
|
const cwd = process.cwd()
|
|
@@ -2128,6 +2120,87 @@ Mipham Code includes 4 agent-type tools for structured workflows:
|
|
|
2128
2120
|
Use /tools to see all 16 tools, including the 4 agent tools.`,
|
|
2129
2121
|
})
|
|
2130
2122
|
|
|
2123
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2124
|
+
// Artifacts
|
|
2125
|
+
// ═══════════════════════════════════════════════════════════════
|
|
2126
|
+
|
|
2127
|
+
async function openBrowser(url: string): Promise<void> {
|
|
2128
|
+
const cmd =
|
|
2129
|
+
process.platform === 'darwin'
|
|
2130
|
+
? `open "${url}"`
|
|
2131
|
+
: process.platform === 'win32'
|
|
2132
|
+
? `start "" "${url}"`
|
|
2133
|
+
: `xdg-open "${url}"`
|
|
2134
|
+
const { exec } = await import('node:child_process')
|
|
2135
|
+
exec(cmd, () => {
|
|
2136
|
+
/* fire-and-forget */
|
|
2137
|
+
})
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
const artifactOpenCmd: CommandHandler = async (ctx, args) => {
|
|
2141
|
+
const name = args[0]
|
|
2142
|
+
if (!name) {
|
|
2143
|
+
return { content: 'Usage: /artifact open <name>\nExample: /artifact open dashboard' }
|
|
2144
|
+
}
|
|
2145
|
+
|
|
2146
|
+
const sessionId = 'session-1' // default session
|
|
2147
|
+
const port = 9876
|
|
2148
|
+
const ext = name.endsWith('.svg') ? '' : '.html'
|
|
2149
|
+
const url = `http://localhost:${port}/${sessionId}/${name}${ext}`
|
|
2150
|
+
|
|
2151
|
+
try {
|
|
2152
|
+
await openBrowser(url)
|
|
2153
|
+
return { content: `✓ Opening artifact "${name}" in browser...\n ${url}` }
|
|
2154
|
+
} catch (err) {
|
|
2155
|
+
return { content: `✗ Failed to open browser: ${String(err)}\n URL: ${url}` }
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
|
|
2159
|
+
const artifactListCmd: CommandHandler = async (_ctx, _args) => {
|
|
2160
|
+
const { getSessionArtifacts } = await import('../artifacts/manifest')
|
|
2161
|
+
const { join } = await import('node:path')
|
|
2162
|
+
const { ARTIFACTS_DIR } = await import('../shared/constants')
|
|
2163
|
+
|
|
2164
|
+
const dir = join(process.cwd(), ARTIFACTS_DIR)
|
|
2165
|
+
const entries = getSessionArtifacts(dir, 'session-1')
|
|
2166
|
+
|
|
2167
|
+
if (entries.length === 0) {
|
|
2168
|
+
return {
|
|
2169
|
+
content: 'No artifacts created yet. Ask the AI to generate one with the Artifact tool.',
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
const lines = ['── Artifacts ──', '']
|
|
2174
|
+
for (const e of entries) {
|
|
2175
|
+
const ver = e.versions && e.versions.length > 1 ? ` v${e.versions.length}` : ''
|
|
2176
|
+
lines.push(
|
|
2177
|
+
` ${e.name.padEnd(24)} ${(e.type + ver).padEnd(8)} ${(e.size / 1024).toFixed(1).padStart(8)}KB ${e.createdAt.slice(0, 16).replace('T', ' ')}`,
|
|
2178
|
+
)
|
|
2179
|
+
}
|
|
2180
|
+
lines.push('', ` ${entries.length} artifact(s) — /artifact open <name> to view`)
|
|
2181
|
+
lines.push(` Gallery: http://localhost:9876`)
|
|
2182
|
+
|
|
2183
|
+
return { content: lines.join('\n') }
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
const artifactServerCmd: CommandHandler = async (_ctx, _args) => {
|
|
2187
|
+
const url = 'http://localhost:9876'
|
|
2188
|
+
return {
|
|
2189
|
+
content: `── Artifact Server ──\n\n URL: ${url}\n Port: 9876\n\nArtifacts are served locally. No external network access.`,
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
const artifactBaseCmd: CommandHandler = (ctx, args) => {
|
|
2194
|
+
const sub = args[0]
|
|
2195
|
+
if (sub === 'open') return artifactOpenCmd(ctx, args.slice(1))
|
|
2196
|
+
if (sub === 'list') return artifactListCmd(ctx, [])
|
|
2197
|
+
if (sub === 'server') return artifactServerCmd(ctx, [])
|
|
2198
|
+
return {
|
|
2199
|
+
content:
|
|
2200
|
+
'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',
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2131
2204
|
// ═══════════════════════════════════════════════════════════════
|
|
2132
2205
|
// Remaining low-priority stubs (backend not yet available)
|
|
2133
2206
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -2214,6 +2287,9 @@ registry.set('/logout', logoutCmd)
|
|
|
2214
2287
|
registry.set('/feedback', feedbackCmd)
|
|
2215
2288
|
registry.set('/agents', agentsCmd)
|
|
2216
2289
|
|
|
2290
|
+
// Artifacts
|
|
2291
|
+
registry.set('/artifact', artifactBaseCmd)
|
|
2292
|
+
|
|
2217
2293
|
// Lower-priority semi-stubs (WIP, backend pending)
|
|
2218
2294
|
registry.set('/branch', branchCmd)
|
|
2219
2295
|
registry.set('/schedule', scheduleCmd)
|
package/src/ui/input.tsx
CHANGED
|
@@ -10,34 +10,95 @@ interface InputBarProps {
|
|
|
10
10
|
// ── Status verbs — English (Claude Code-aligned) + Chinese (Mipham originals) ──
|
|
11
11
|
|
|
12
12
|
const STATUS_EN = [
|
|
13
|
-
'Doodling',
|
|
14
|
-
'
|
|
15
|
-
'
|
|
16
|
-
'
|
|
17
|
-
'
|
|
18
|
-
'
|
|
19
|
-
'
|
|
20
|
-
'
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
13
|
+
'Doodling',
|
|
14
|
+
'Forging',
|
|
15
|
+
'Germinating',
|
|
16
|
+
'Mosmosing',
|
|
17
|
+
'Cerebrating',
|
|
18
|
+
'Boogieing',
|
|
19
|
+
'Finagling',
|
|
20
|
+
'Misting',
|
|
21
|
+
'Recombobulating',
|
|
22
|
+
'Slithering',
|
|
23
|
+
'Effecting',
|
|
24
|
+
'Roosting',
|
|
25
|
+
'Crystallizing',
|
|
26
|
+
'Canoodling',
|
|
27
|
+
'Billowing',
|
|
28
|
+
'Warping',
|
|
29
|
+
'Improvising',
|
|
30
|
+
'Metamorphing',
|
|
31
|
+
'Gusting',
|
|
32
|
+
'Whiring',
|
|
33
|
+
'Topsy-turvying',
|
|
34
|
+
'Flibbertigibetting',
|
|
35
|
+
'Wibbling',
|
|
36
|
+
'Thundering',
|
|
37
|
+
'Moseying',
|
|
38
|
+
'Julienning',
|
|
39
|
+
'Evaporating',
|
|
40
|
+
'Ruminating',
|
|
41
|
+
'Whisking',
|
|
42
|
+
'Scampering',
|
|
43
|
+
'Meandering',
|
|
44
|
+
'Concoting',
|
|
45
|
+
'Gesticulating',
|
|
46
|
+
'Flamebeing',
|
|
47
|
+
'Quantumizing',
|
|
48
|
+
'Waddling',
|
|
49
|
+
'Fluttering',
|
|
50
|
+
'Sprouting',
|
|
51
|
+
'Elucidating',
|
|
52
|
+
'Embellishing',
|
|
53
|
+
'Razzmatizing',
|
|
54
|
+
'Pondering',
|
|
55
|
+
'Cogitating',
|
|
56
|
+
'Garnishing',
|
|
57
|
+
'Actualizing',
|
|
58
|
+
'Zigzagging',
|
|
59
|
+
'Mustering',
|
|
60
|
+
'Frolicking',
|
|
61
|
+
'Hullaballooing',
|
|
62
|
+
'Doing',
|
|
63
|
+
'Fermenting',
|
|
64
|
+
'Considering',
|
|
65
|
+
'Skedaddling',
|
|
66
|
+
'Actioning',
|
|
67
|
+
'Orbiting',
|
|
68
|
+
'Perambulating',
|
|
69
|
+
'Drizzling',
|
|
70
|
+
'Schlepping',
|
|
71
|
+
'Ionizing',
|
|
25
72
|
'Scurrying',
|
|
26
73
|
]
|
|
27
74
|
|
|
28
75
|
const STATUS_CN = [
|
|
29
|
-
'思忖中',
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
'
|
|
76
|
+
'思忖中',
|
|
77
|
+
'推演中',
|
|
78
|
+
'凝炼中',
|
|
79
|
+
'熬煮中',
|
|
80
|
+
'锻造中',
|
|
81
|
+
'研磨中',
|
|
82
|
+
'解构中',
|
|
83
|
+
'冥思中',
|
|
84
|
+
'编织中',
|
|
85
|
+
'萃取中',
|
|
86
|
+
'烹制中',
|
|
87
|
+
'淬火中',
|
|
88
|
+
'雕琢中',
|
|
89
|
+
'融汇中',
|
|
90
|
+
'觉照中',
|
|
91
|
+
'运转中',
|
|
92
|
+
'参详中',
|
|
93
|
+
'化合中',
|
|
94
|
+
'浸润中',
|
|
95
|
+
'焙烤中',
|
|
33
96
|
]
|
|
34
97
|
|
|
35
98
|
// Merge both — bilingual status verbs
|
|
36
99
|
const STATUS_GERUNDS = [...STATUS_EN, ...STATUS_CN]
|
|
37
100
|
|
|
38
|
-
const STATUS_PAST = [
|
|
39
|
-
'Brewed', 'Churned', 'Cooked', 'Sautéed', 'Cogitated', 'Crunched',
|
|
40
|
-
]
|
|
101
|
+
const STATUS_PAST = ['Brewed', 'Churned', 'Cooked', 'Sautéed', 'Cogitated', 'Crunched']
|
|
41
102
|
|
|
42
103
|
function pick<T>(arr: T[]): T {
|
|
43
104
|
return arr[Math.floor(Math.random() * arr.length)]!
|
package/src/ui/picker.tsx
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import React, { useState, useCallback
|
|
1
|
+
import React, { useState, useCallback } from 'react'
|
|
2
2
|
import { Box, Text, useInput } from 'ink'
|
|
3
|
-
import type { MiphamConfig, ProviderConfig
|
|
3
|
+
import type { MiphamConfig, ProviderConfig } from '../shared/index.ts'
|
|
4
4
|
|
|
5
5
|
interface PickerProps {
|
|
6
6
|
config: MiphamConfig
|
|
@@ -113,9 +113,6 @@ export function ModelPicker({
|
|
|
113
113
|
// Provider labels
|
|
114
114
|
const providerColor = (p: ProviderConfig) => (p.id === currentProvider ? 'green' : 'white')
|
|
115
115
|
|
|
116
|
-
const modelColor = (m: ModelInfo) =>
|
|
117
|
-
selectedProvider?.id === currentProvider && m.id === currentModel ? 'green' : 'white'
|
|
118
|
-
|
|
119
116
|
return (
|
|
120
117
|
<Box flexDirection="column" borderStyle="round" borderColor="cyan" padding={1}>
|
|
121
118
|
{/* Title */}
|