miii-cli 0.1.7 → 0.1.8
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/.claude/settings.local.json +3 -1
- package/dist/init.js +4 -0
- package/dist/init.js.map +1 -1
- package/dist/llm/ollama.d.ts +1 -0
- package/dist/llm/ollama.js +49 -0
- package/dist/llm/ollama.js.map +1 -1
- package/dist/parser/stream-parser.d.ts +4 -0
- package/dist/parser/stream-parser.js +170 -3
- package/dist/parser/stream-parser.js.map +1 -1
- package/dist/tools/index.js +42 -6
- package/dist/tools/index.js.map +1 -1
- package/dist/tui/App.js +54 -5
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/InputBar.js +41 -1
- package/dist/tui/InputBar.js.map +1 -1
- package/dist/tui/components/InputArea.d.ts +2 -1
- package/dist/tui/components/InputArea.js +14 -4
- package/dist/tui/components/InputArea.js.map +1 -1
- package/dist/tui/printer.js +9 -1
- package/dist/tui/printer.js.map +1 -1
- package/package.json +1 -1
- package/src/parser/stream-parser.ts +145 -3
- package/src/tools/index.ts +40 -6
- package/src/tui/App.tsx +71 -3
- package/src/tui/printer.ts +10 -1
package/src/tui/App.tsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useCallback, useRef, useEffect } from 'react'
|
|
2
|
-
import { Box, useStdout, useInput } from 'ink'
|
|
2
|
+
import { Box, Text, useStdout, useInput } from 'ink'
|
|
3
3
|
import { StatusBar, Divider } from './components/StatusBar.js'
|
|
4
4
|
import { MessageList } from './components/MessageList.js'
|
|
5
5
|
import { InputArea } from './components/InputArea.js'
|
|
@@ -7,7 +7,7 @@ import { ModelPicker } from './components/ModelPicker.js'
|
|
|
7
7
|
import { stream } from '../llm/stream.js'
|
|
8
8
|
import { listModels, pullModel } from '../llm/ollama.js'
|
|
9
9
|
import type { OllamaModel } from '../llm/ollama.js'
|
|
10
|
-
import { StreamParser } from '../parser/stream-parser.js'
|
|
10
|
+
import { StreamParser, extractBareToolCall } from '../parser/stream-parser.js'
|
|
11
11
|
import { tools, getSystemPrompt } from '../tools/index.js'
|
|
12
12
|
import { readFile, guardPath } from '../files/ops.js'
|
|
13
13
|
import type { SkillLoader } from '../skills/loader.js'
|
|
@@ -66,10 +66,18 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
66
66
|
const tokenBufRef = useRef('')
|
|
67
67
|
const lastRenderRef = useRef(0)
|
|
68
68
|
const messagesRef = useRef(messages)
|
|
69
|
+
const approvalResolveRef = useRef<((ok: boolean) => void) | null>(null)
|
|
70
|
+
const [pendingApproval, setPendingApproval] = useState<{
|
|
71
|
+
toolName: string
|
|
72
|
+
path: string
|
|
73
|
+
content?: string
|
|
74
|
+
} | null>(null)
|
|
75
|
+
const pendingApprovalRef = useRef(pendingApproval)
|
|
69
76
|
|
|
70
77
|
useEffect(() => { systemPromptRef.current = systemPrompt }, [systemPrompt])
|
|
71
78
|
useEffect(() => { currentModelRef.current = currentModel }, [currentModel])
|
|
72
79
|
useEffect(() => { messagesRef.current = messages }, [messages])
|
|
80
|
+
useEffect(() => { pendingApprovalRef.current = pendingApproval }, [pendingApproval])
|
|
73
81
|
|
|
74
82
|
useEffect(() => {
|
|
75
83
|
if (status === 'idle') return
|
|
@@ -80,6 +88,20 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
80
88
|
// Scroll keybindings — PageUp/PageDn scroll message history
|
|
81
89
|
const SCROLL_STEP = 5
|
|
82
90
|
useInput((_input, key) => {
|
|
91
|
+
// approvalResolveRef is set synchronously in requestApproval — no useEffect needed
|
|
92
|
+
if (approvalResolveRef.current) {
|
|
93
|
+
const resolve = approvalResolveRef.current
|
|
94
|
+
if (_input === 'y' || _input === 'Y') {
|
|
95
|
+
approvalResolveRef.current = null
|
|
96
|
+
setPendingApproval(null)
|
|
97
|
+
resolve(true)
|
|
98
|
+
} else if (_input === 'n' || _input === 'N' || key.escape) {
|
|
99
|
+
approvalResolveRef.current = null
|
|
100
|
+
setPendingApproval(null)
|
|
101
|
+
resolve(false)
|
|
102
|
+
}
|
|
103
|
+
return
|
|
104
|
+
}
|
|
83
105
|
if (pickerOpen) return
|
|
84
106
|
if (key.pageUp) {
|
|
85
107
|
setScrollOffset(n => Math.min(n + SCROLL_STEP, Math.max(0, messages.length - 1)))
|
|
@@ -92,6 +114,19 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
92
114
|
const cols = stdout.columns ?? 80
|
|
93
115
|
const rows = stdout.rows ?? 24
|
|
94
116
|
|
|
117
|
+
const APPROVAL_TOOLS = new Set(['delete_file'])
|
|
118
|
+
|
|
119
|
+
const requestApproval = useCallback((toolName: string, args: Record<string, unknown>): Promise<boolean> => {
|
|
120
|
+
return new Promise((resolve) => {
|
|
121
|
+
approvalResolveRef.current = resolve
|
|
122
|
+
setPendingApproval({
|
|
123
|
+
toolName,
|
|
124
|
+
path: ((args.path ?? args.from) as string) ?? '',
|
|
125
|
+
content: args.content as string | undefined,
|
|
126
|
+
})
|
|
127
|
+
})
|
|
128
|
+
}, [])
|
|
129
|
+
|
|
95
130
|
function addMsg(role: Message['role'], content: string, id?: string): string {
|
|
96
131
|
const mid = id ?? generateId()
|
|
97
132
|
setMessages(prev => [...prev, { id: mid, role, content, timestamp: Date.now() }])
|
|
@@ -154,7 +189,19 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
154
189
|
if (item.type === 'tool_call') pendingTools.push({ name: item.toolName, args: item.toolArgs })
|
|
155
190
|
}
|
|
156
191
|
|
|
157
|
-
if (!pendingTools.length) {
|
|
192
|
+
if (!pendingTools.length) {
|
|
193
|
+
// Fallback: LLM emitted bare JSON without <tool_call> wrapper
|
|
194
|
+
const bare = extractBareToolCall(fullText)
|
|
195
|
+
if (bare) {
|
|
196
|
+
pendingTools.push(bare)
|
|
197
|
+
} else {
|
|
198
|
+
if (fullText.includes('{"name"')) {
|
|
199
|
+
addMsg('tool', 'tool_call parse failed — could not extract tool call from model output')
|
|
200
|
+
}
|
|
201
|
+
setStatus('idle')
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
}
|
|
158
205
|
|
|
159
206
|
setStatus('tool')
|
|
160
207
|
const next: ChatMessage[] = [...contextMsgs, { role: 'assistant', content: fullText }]
|
|
@@ -163,6 +210,15 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
163
210
|
const tool = tools.find(t => t.name === tc.name)
|
|
164
211
|
const toolId = generateId()
|
|
165
212
|
if (tool) {
|
|
213
|
+
if (APPROVAL_TOOLS.has(tc.name)) {
|
|
214
|
+
const approved = await requestApproval(tc.name, tc.args)
|
|
215
|
+
if (!approved) {
|
|
216
|
+
const cancelled = `[${tc.name}] cancelled by user`
|
|
217
|
+
setMessages(prev => [...prev, { id: toolId, role: 'tool', content: cancelled, timestamp: Date.now() }])
|
|
218
|
+
next.push({ role: 'user', content: `Tool ${tc.name} was cancelled by user.` })
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
}
|
|
166
222
|
try {
|
|
167
223
|
const result = await tool.execute(tc.args)
|
|
168
224
|
setMessages(prev => [...prev, { id: toolId, role: 'tool', content: `[${tc.name}]\n${result}`, timestamp: Date.now() }])
|
|
@@ -298,6 +354,18 @@ export function App({ config, skills, cwd }: Props) {
|
|
|
298
354
|
/>
|
|
299
355
|
)}
|
|
300
356
|
<Divider cols={cols} />
|
|
357
|
+
{pendingApproval && (
|
|
358
|
+
<Box flexDirection="column" borderStyle="round" borderColor="yellow" paddingX={1} marginBottom={1}>
|
|
359
|
+
<Text color="yellow" bold>Allow {pendingApproval.toolName}?</Text>
|
|
360
|
+
<Text> path: <Text color="cyan">{pendingApproval.path}</Text></Text>
|
|
361
|
+
{pendingApproval.content && (
|
|
362
|
+
<Text color="gray" dimColor>
|
|
363
|
+
{pendingApproval.content.split('\n').slice(0, 12).join('\n')}
|
|
364
|
+
</Text>
|
|
365
|
+
)}
|
|
366
|
+
<Text color="green">[y] approve <Text color="red">[n] cancel</Text></Text>
|
|
367
|
+
</Box>
|
|
368
|
+
)}
|
|
301
369
|
<InputArea
|
|
302
370
|
status={status}
|
|
303
371
|
skills={skillList}
|
package/src/tui/printer.ts
CHANGED
|
@@ -18,6 +18,15 @@ function indent(text: string, pad = ' '): string {
|
|
|
18
18
|
return text.split('\n').map(l => pad + l).join('\n')
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
function stripMarkdown(s: string): string {
|
|
22
|
+
return s
|
|
23
|
+
.replace(/\*\*\*(.+?)\*\*\*/g, '$1')
|
|
24
|
+
.replace(/\*\*(.+?)\*\*/g, '$1')
|
|
25
|
+
.replace(/\*(.+?)\*/g, '$1')
|
|
26
|
+
.replace(/`([^`]+)`/g, '$1')
|
|
27
|
+
.replace(/^#{1,6} /gm, '')
|
|
28
|
+
}
|
|
29
|
+
|
|
21
30
|
function formatContent(text: string): string {
|
|
22
31
|
const lines = text.split('\n')
|
|
23
32
|
let inCode = false
|
|
@@ -30,7 +39,7 @@ function formatContent(text: string): string {
|
|
|
30
39
|
} else if (inCode) {
|
|
31
40
|
out.push(' ' + yellow(line || ' '))
|
|
32
41
|
} else {
|
|
33
|
-
out.push(' ' + (line || ''))
|
|
42
|
+
out.push(' ' + stripMarkdown(line || ''))
|
|
34
43
|
}
|
|
35
44
|
}
|
|
36
45
|
return out.join('\n')
|