miii-cli 0.1.6 → 0.1.7
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/package.json +1 -1
- package/src/init.ts +5 -0
- package/src/llm/ollama.ts +52 -0
- package/src/tui/InputBar.tsx +46 -0
- package/src/tui/components/InputArea.tsx +15 -3
package/package.json
CHANGED
package/src/init.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { loadConfig } from './config.js'
|
|
|
5
5
|
import { SkillLoader } from './skills/loader.js'
|
|
6
6
|
import { InputBar } from './tui/InputBar.js'
|
|
7
7
|
import { welcome } from './tui/printer.js'
|
|
8
|
+
import { ensureOllama } from './llm/ollama.js'
|
|
8
9
|
|
|
9
10
|
export async function lazyInit(): Promise<void> {
|
|
10
11
|
const argv = minimist(process.argv.slice(2), {
|
|
@@ -17,6 +18,10 @@ export async function lazyInit(): Promise<void> {
|
|
|
17
18
|
if (argv.url) config.baseUrl = argv.url
|
|
18
19
|
if (argv.provider) config.provider = argv.provider as typeof config.provider
|
|
19
20
|
|
|
21
|
+
if (config.provider === 'ollama') {
|
|
22
|
+
await ensureOllama(config.baseUrl)
|
|
23
|
+
}
|
|
24
|
+
|
|
20
25
|
const skills = new SkillLoader()
|
|
21
26
|
await skills.loadAll()
|
|
22
27
|
|
package/src/llm/ollama.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { execSync, spawn } from 'child_process'
|
|
2
|
+
|
|
1
3
|
export interface OllamaModel {
|
|
2
4
|
name: string
|
|
3
5
|
size: number
|
|
@@ -51,6 +53,56 @@ export async function pullModel(
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
export async function ensureOllama(baseUrl: string): Promise<void> {
|
|
57
|
+
if (await isReachable(baseUrl)) return
|
|
58
|
+
|
|
59
|
+
if (!isBinaryInstalled()) {
|
|
60
|
+
process.stderr.write('\nOllama not found. Install it:\n\n')
|
|
61
|
+
if (process.platform === 'darwin') {
|
|
62
|
+
process.stderr.write(' brew install ollama\n')
|
|
63
|
+
process.stderr.write(' — or download: https://ollama.ai/download\n')
|
|
64
|
+
} else if (process.platform === 'linux') {
|
|
65
|
+
process.stderr.write(' curl -fsSL https://ollama.ai/install.sh | sh\n')
|
|
66
|
+
} else {
|
|
67
|
+
process.stderr.write(' https://ollama.ai/download\n')
|
|
68
|
+
}
|
|
69
|
+
process.stderr.write('\nThen run: ollama serve\n\n')
|
|
70
|
+
process.exit(1)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
process.stderr.write('Ollama not running — starting ollama serve...\n')
|
|
74
|
+
spawn('ollama', ['serve'], { detached: true, stdio: 'ignore' }).unref()
|
|
75
|
+
|
|
76
|
+
for (let i = 0; i < 12; i++) {
|
|
77
|
+
await new Promise(r => setTimeout(r, 500))
|
|
78
|
+
if (await isReachable(baseUrl)) {
|
|
79
|
+
process.stderr.write('Ollama ready.\n')
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.stderr.write('Could not start Ollama. Run manually: ollama serve\n')
|
|
85
|
+
process.exit(1)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function isReachable(baseUrl: string): Promise<boolean> {
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(`${baseUrl}/api/tags`, { signal: AbortSignal.timeout(2000) })
|
|
91
|
+
return res.ok
|
|
92
|
+
} catch {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isBinaryInstalled(): boolean {
|
|
98
|
+
try {
|
|
99
|
+
execSync(process.platform === 'win32' ? 'where ollama' : 'which ollama', { stdio: 'ignore' })
|
|
100
|
+
return true
|
|
101
|
+
} catch {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
54
106
|
export function fmtSize(bytes: number): string {
|
|
55
107
|
if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)}GB`
|
|
56
108
|
if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(0)}MB`
|
package/src/tui/InputBar.tsx
CHANGED
|
@@ -51,6 +51,7 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
51
51
|
const [currentModel, setCurrentModel] = useState(config.model)
|
|
52
52
|
const [streamPreview, setStreamPreview] = useState('')
|
|
53
53
|
const [sessionName, setSessionName] = useState(session)
|
|
54
|
+
const [planningMode, setPlanningMode] = useState(false)
|
|
54
55
|
|
|
55
56
|
// picker opens on mount — force model selection every launch
|
|
56
57
|
const [pickerOpen, setPickerOpen] = useState(true)
|
|
@@ -222,6 +223,8 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
222
223
|
const newName = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-')
|
|
223
224
|
historyRef.current = []
|
|
224
225
|
setSessionName(newName)
|
|
226
|
+
setPlanningMode(false)
|
|
227
|
+
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`)
|
|
225
228
|
printer.systemMsg(`new session → ${newName}`)
|
|
226
229
|
return
|
|
227
230
|
}
|
|
@@ -229,12 +232,54 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
229
232
|
if (cmd === '/clear') {
|
|
230
233
|
historyRef.current = []
|
|
231
234
|
saveSession(sessionNameRef.current, [])
|
|
235
|
+
setPlanningMode(false)
|
|
236
|
+
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`)
|
|
232
237
|
printer.systemMsg('chat cleared')
|
|
233
238
|
return
|
|
234
239
|
}
|
|
235
240
|
|
|
236
241
|
if (cmd === '/exit') { process.exit(0) }
|
|
237
242
|
|
|
243
|
+
if (cmd === '/plan' || cmd.startsWith('/plan ')) {
|
|
244
|
+
const topic = cmd.slice(5).trim()
|
|
245
|
+
setPlanningMode(true)
|
|
246
|
+
systemPromptRef.current = getSystemPrompt(
|
|
247
|
+
`\n- CWD: ${cwd}\n- MODE: Planning assistant. Help the user plan step by step. Ask clarifying questions. Suggest concrete next steps. Use plain text only — no markdown, no headers, no bold, no bullets with asterisks, no backtick blocks. Use numbered lists and plain indentation for structure.`
|
|
248
|
+
)
|
|
249
|
+
const msg = topic
|
|
250
|
+
? `I want to plan: ${topic}`
|
|
251
|
+
: 'I want to start planning. Help me think through my goals step by step.'
|
|
252
|
+
printer.userMsg(msg)
|
|
253
|
+
historyRef.current.push({ role: 'user', content: msg })
|
|
254
|
+
saveSession(sessionNameRef.current, historyRef.current)
|
|
255
|
+
await runLoop(buildContext())
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (cmd === '/plan:done') {
|
|
260
|
+
setPlanningMode(false)
|
|
261
|
+
systemPromptRef.current = getSystemPrompt(`\n- CWD: ${cwd}`)
|
|
262
|
+
printer.systemMsg('planning mode off')
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (cmd.startsWith('/plan:')) {
|
|
267
|
+
const subCmd = cmd.slice(6)
|
|
268
|
+
const subPrompts: Record<string, string> = {
|
|
269
|
+
next: 'What are the next concrete steps I should take?',
|
|
270
|
+
breakdown: 'Can you break this down into specific subtasks?',
|
|
271
|
+
review: 'Please review and critique our plan so far. What are we missing?',
|
|
272
|
+
}
|
|
273
|
+
const msg = subPrompts[subCmd]
|
|
274
|
+
if (msg) {
|
|
275
|
+
printer.userMsg(msg)
|
|
276
|
+
historyRef.current.push({ role: 'user', content: msg })
|
|
277
|
+
saveSession(sessionNameRef.current, historyRef.current)
|
|
278
|
+
await runLoop(buildContext())
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
238
283
|
if (cmd === '/sessions') {
|
|
239
284
|
const sessions = listSessions()
|
|
240
285
|
if (!sessions.length) { printer.systemMsg('no saved sessions'); return }
|
|
@@ -339,6 +384,7 @@ export function InputBar({ config, skills, cwd, session }: Props) {
|
|
|
339
384
|
status={status}
|
|
340
385
|
skills={skillList}
|
|
341
386
|
cwd={cwd}
|
|
387
|
+
planningMode={planningMode}
|
|
342
388
|
onSubmit={handleSubmit}
|
|
343
389
|
onAbort={handleAbort}
|
|
344
390
|
/>
|
|
@@ -16,6 +16,14 @@ const BUILTIN_COMMANDS: Skill[] = [
|
|
|
16
16
|
{ ns: 'builtin', name: 'session', description: 'switch session /session <name>' },
|
|
17
17
|
{ ns: 'builtin', name: 'exit', description: 'exit miii' },
|
|
18
18
|
{ ns: 'builtin', name: 'list', description: 'list all loaded skills' },
|
|
19
|
+
{ ns: 'builtin', name: 'plan', description: 'start planning mode /plan [topic]' },
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
const PLANNING_COMMANDS: Skill[] = [
|
|
23
|
+
{ ns: 'plan', name: 'next', description: 'suggest next concrete steps' },
|
|
24
|
+
{ ns: 'plan', name: 'breakdown', description: 'break current topic into subtasks' },
|
|
25
|
+
{ ns: 'plan', name: 'review', description: 'review and critique the plan so far' },
|
|
26
|
+
{ ns: 'plan', name: 'done', description: 'exit planning mode' },
|
|
19
27
|
]
|
|
20
28
|
|
|
21
29
|
type Overlay = 'none' | 'command' | 'at'
|
|
@@ -24,11 +32,12 @@ interface Props {
|
|
|
24
32
|
status: Status
|
|
25
33
|
skills: Skill[]
|
|
26
34
|
cwd: string
|
|
35
|
+
planningMode?: boolean
|
|
27
36
|
onSubmit: (text: string) => void
|
|
28
37
|
onAbort: () => void
|
|
29
38
|
}
|
|
30
39
|
|
|
31
|
-
export function InputArea({ status, skills, cwd, onSubmit, onAbort }: Props) {
|
|
40
|
+
export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort }: Props) {
|
|
32
41
|
const [lines, setLines] = useState<string[]>([''])
|
|
33
42
|
const [cursor, setCursor] = useState({ row: 0, col: 0 })
|
|
34
43
|
const [overlay, setOverlay] = useState<Overlay>('none')
|
|
@@ -42,8 +51,9 @@ export function InputArea({ status, skills, cwd, onSubmit, onAbort }: Props) {
|
|
|
42
51
|
const allCommands = useMemo(() => {
|
|
43
52
|
const builtinNames = new Set(BUILTIN_COMMANDS.map(b => b.name))
|
|
44
53
|
const userSkills = skills.filter(s => !builtinNames.has(s.name))
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
const base = [...BUILTIN_COMMANDS, ...userSkills]
|
|
55
|
+
return planningMode ? [...PLANNING_COMMANDS, ...base] : base
|
|
56
|
+
}, [skills, planningMode])
|
|
47
57
|
|
|
48
58
|
const isActive = status === 'idle'
|
|
49
59
|
const fullInput = lines.join('\n')
|
|
@@ -245,6 +255,8 @@ export function InputArea({ status, skills, cwd, onSubmit, onAbort }: Props) {
|
|
|
245
255
|
? '↑↓ navigate enter select esc close'
|
|
246
256
|
: overlay === 'at'
|
|
247
257
|
? '↑↓ navigate enter select esc close'
|
|
258
|
+
: planningMode
|
|
259
|
+
? '📋 planning mode / suggestions enter send /plan:done to exit'
|
|
248
260
|
: '@ file / command enter send ctrl+c exit'
|
|
249
261
|
|
|
250
262
|
return (
|