miii-cli 0.2.1 → 0.2.2
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 +6 -1
- package/.claude/settings.local.json +0 -28
- package/CONTRIBUTING.md +0 -55
- package/Makefile +0 -13
- package/install.sh +0 -6
- package/mii-cli.gif +0 -0
- package/src/config.ts +0 -32
- package/src/files/ops.ts +0 -89
- package/src/index.ts +0 -11
- package/src/init.ts +0 -41
- package/src/llm/ollama.ts +0 -110
- package/src/llm/stream.ts +0 -55
- package/src/parser/stream-parser.ts +0 -196
- package/src/sessions.ts +0 -54
- package/src/skills/loader.ts +0 -144
- package/src/tools/index.ts +0 -151
- package/src/tui/App.tsx +0 -355
- package/src/tui/InputBar.tsx +0 -381
- package/src/tui/components/AtPicker.tsx +0 -49
- package/src/tui/components/CommandPalette.tsx +0 -50
- package/src/tui/components/InputArea.tsx +0 -297
- package/src/tui/components/MessageList.tsx +0 -219
- package/src/tui/components/ModelPicker.tsx +0 -134
- package/src/tui/components/StatusBar.tsx +0 -36
- package/src/tui/printer.ts +0 -130
- package/src/types.ts +0 -26
- package/src/workers/context.worker.ts +0 -66
- package/src/workers/diff.worker.ts +0 -20
- package/src/workers/spawn.ts +0 -19
- package/tsconfig.json +0 -18
package/src/sessions.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync } from 'fs'
|
|
2
|
-
import { join } from 'path'
|
|
3
|
-
import { homedir } from 'os'
|
|
4
|
-
import type { ChatMessage } from './types.js'
|
|
5
|
-
|
|
6
|
-
const SESSIONS_DIR = join(homedir(), '.config', 'miii', 'sessions')
|
|
7
|
-
|
|
8
|
-
function ensureDir() {
|
|
9
|
-
mkdirSync(SESSIONS_DIR, { recursive: true })
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function sanitizeName(name: string): string {
|
|
13
|
-
if (!/^[\w-]+$/.test(name)) throw new Error(`invalid session name: ${name}`)
|
|
14
|
-
return name
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function listSessions(): Array<{ name: string; messageCount: number; updatedAt: number }> {
|
|
18
|
-
ensureDir()
|
|
19
|
-
return readdirSync(SESSIONS_DIR)
|
|
20
|
-
.filter(f => f.endsWith('.json'))
|
|
21
|
-
.map(f => {
|
|
22
|
-
const name = f.replace('.json', '')
|
|
23
|
-
const p = join(SESSIONS_DIR, f)
|
|
24
|
-
let messageCount = 0
|
|
25
|
-
let updatedAt = 0
|
|
26
|
-
try {
|
|
27
|
-
updatedAt = statSync(p).mtimeMs
|
|
28
|
-
const msgs = JSON.parse(readFileSync(p, 'utf-8'))
|
|
29
|
-
messageCount = Array.isArray(msgs) ? msgs.length : 0
|
|
30
|
-
} catch {}
|
|
31
|
-
return { name, messageCount, updatedAt }
|
|
32
|
-
})
|
|
33
|
-
.sort((a, b) => b.updatedAt - a.updatedAt)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function loadSession(name: string): ChatMessage[] {
|
|
37
|
-
ensureDir()
|
|
38
|
-
const p = join(SESSIONS_DIR, `${sanitizeName(name)}.json`)
|
|
39
|
-
if (!existsSync(p)) return []
|
|
40
|
-
try {
|
|
41
|
-
const parsed = JSON.parse(readFileSync(p, 'utf-8'))
|
|
42
|
-
return Array.isArray(parsed) ? parsed : []
|
|
43
|
-
} catch { return [] }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function saveSession(name: string, messages: ChatMessage[]) {
|
|
47
|
-
ensureDir()
|
|
48
|
-
writeFileSync(join(SESSIONS_DIR, `${sanitizeName(name)}.json`), JSON.stringify(messages))
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function deleteSession(name: string) {
|
|
52
|
-
const p = join(SESSIONS_DIR, `${sanitizeName(name)}.json`)
|
|
53
|
-
if (existsSync(p)) unlinkSync(p)
|
|
54
|
-
}
|
package/src/skills/loader.ts
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync, readdirSync } from 'fs'
|
|
2
|
-
import { join, basename } from 'path'
|
|
3
|
-
import { homedir } from 'os'
|
|
4
|
-
import { createDir, moveFile, writeFile, guardPath } from '../files/ops.js'
|
|
5
|
-
|
|
6
|
-
export interface SkillContext {
|
|
7
|
-
messages: Array<{ role: string; content: string }>
|
|
8
|
-
appendMessage: (role: string, content: string) => void
|
|
9
|
-
setSystemPrompt: (p: string) => void
|
|
10
|
-
getSystemPrompt: () => string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface Skill {
|
|
14
|
-
name: string
|
|
15
|
-
ns: string
|
|
16
|
-
description: string
|
|
17
|
-
prompt?: string
|
|
18
|
-
execute?: (args: string, ctx: SkillContext) => string | Promise<string>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const builtin: Skill[] = [
|
|
22
|
-
{
|
|
23
|
-
name: 'caveman',
|
|
24
|
-
ns: 'caveman',
|
|
25
|
-
description: 'Ultra-compressed terse mode',
|
|
26
|
-
execute: (_, ctx) => {
|
|
27
|
-
const cur = ctx.getSystemPrompt()
|
|
28
|
-
ctx.setSystemPrompt(cur + '\n\nRespond ultra-compressed. Drop articles, filler, pleasantries, hedging. Fragments OK. Technical terms exact.')
|
|
29
|
-
return 'Caveman mode active.'
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
name: 'normal',
|
|
34
|
-
ns: 'caveman',
|
|
35
|
-
description: 'Revert to normal tone',
|
|
36
|
-
execute: (_, ctx) => {
|
|
37
|
-
const cur = ctx.getSystemPrompt()
|
|
38
|
-
ctx.setSystemPrompt(cur.replace(/\n\nRespond ultra-compressed.*$/s, ''))
|
|
39
|
-
return 'Normal mode.'
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
name: 'review',
|
|
44
|
-
ns: 'default',
|
|
45
|
-
description: 'Review codebase for bugs/security/quality',
|
|
46
|
-
prompt: 'Review the code in this project. Look for bugs, security issues, performance problems, and quality issues. Be specific and actionable. List findings grouped by severity.',
|
|
47
|
-
},
|
|
48
|
-
{
|
|
49
|
-
name: 'help',
|
|
50
|
-
ns: 'default',
|
|
51
|
-
description: 'Show available commands',
|
|
52
|
-
execute: (_, ctx) => {
|
|
53
|
-
return 'Built-in: /review /mkdir /mv /touch /models /sessions /session /clear /list /help\nType /list for all loaded skills.'
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'list',
|
|
58
|
-
ns: 'default',
|
|
59
|
-
description: 'List all skills',
|
|
60
|
-
execute: () => '', // handled dynamically in loader.list()
|
|
61
|
-
},
|
|
62
|
-
{
|
|
63
|
-
name: 'models',
|
|
64
|
-
ns: 'default',
|
|
65
|
-
description: 'Choose or pull Ollama models',
|
|
66
|
-
// execute handled specially in App.tsx before skill lookup
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
name: 'mkdir',
|
|
70
|
-
ns: 'default',
|
|
71
|
-
description: 'Create a folder — usage: /mkdir <path>',
|
|
72
|
-
execute: (args) => {
|
|
73
|
-
const p = args.trim()
|
|
74
|
-
if (!p) return 'Usage: /mkdir <path>'
|
|
75
|
-
createDir(guardPath(p))
|
|
76
|
-
return `created: ${p}`
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: 'mv',
|
|
81
|
-
ns: 'default',
|
|
82
|
-
description: 'Move or rename file/folder — usage: /mv <from> <to>',
|
|
83
|
-
execute: (args) => {
|
|
84
|
-
const parts = args.trim().split(/\s+/)
|
|
85
|
-
if (parts.length < 2) return 'Usage: /mv <from> <to>'
|
|
86
|
-
const [from, to] = parts
|
|
87
|
-
moveFile(guardPath(from), guardPath(to))
|
|
88
|
-
return `moved: ${from} → ${to}`
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: 'touch',
|
|
93
|
-
ns: 'default',
|
|
94
|
-
description: 'Create empty file — usage: /touch <path>',
|
|
95
|
-
execute: (args) => {
|
|
96
|
-
const p = args.trim()
|
|
97
|
-
if (!p) return 'Usage: /touch <path>'
|
|
98
|
-
writeFile(guardPath(p), '')
|
|
99
|
-
return `created: ${p}`
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
export class SkillLoader {
|
|
105
|
-
private map = new Map<string, Skill>()
|
|
106
|
-
|
|
107
|
-
constructor() {
|
|
108
|
-
for (const s of builtin) {
|
|
109
|
-
this.map.set(`${s.ns}:${s.name}`, s)
|
|
110
|
-
this.map.set(s.name, s)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
async loadAll(): Promise<void> {
|
|
115
|
-
const dirs = [
|
|
116
|
-
join(homedir(), '.config', 'miii', 'skills'),
|
|
117
|
-
join(process.cwd(), '.miii', 'skills'),
|
|
118
|
-
]
|
|
119
|
-
for (const dir of dirs) {
|
|
120
|
-
if (!existsSync(dir)) continue
|
|
121
|
-
for (const entry of readdirSync(dir)) {
|
|
122
|
-
if (!entry.endsWith('.md')) continue
|
|
123
|
-
const name = basename(entry, '.md')
|
|
124
|
-
const content = readFileSync(join(dir, entry), 'utf-8')
|
|
125
|
-
const skill: Skill = {
|
|
126
|
-
name,
|
|
127
|
-
ns: 'custom',
|
|
128
|
-
description: content.split('\n')[0].replace(/^#+\s*/, '').trim(),
|
|
129
|
-
prompt: content,
|
|
130
|
-
}
|
|
131
|
-
this.map.set(name, skill)
|
|
132
|
-
this.map.set(`custom:${name}`, skill)
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
get(ref: string): Skill | undefined {
|
|
138
|
-
return this.map.get(ref)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
list(): Skill[] {
|
|
142
|
-
return [...new Set(this.map.values())]
|
|
143
|
-
}
|
|
144
|
-
}
|
package/src/tools/index.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { readFile, writeFile, deleteFile, listFiles, createDir, moveFile, guardPath } from '../files/ops.js'
|
|
2
|
-
import { existsSync } from 'fs'
|
|
3
|
-
import { exec } from 'child_process'
|
|
4
|
-
import { promisify } from 'util'
|
|
5
|
-
|
|
6
|
-
const run = promisify(exec)
|
|
7
|
-
const EXEC_TIMEOUT_MS = 30_000
|
|
8
|
-
|
|
9
|
-
export interface Tool {
|
|
10
|
-
name: string
|
|
11
|
-
description: string
|
|
12
|
-
params: string
|
|
13
|
-
execute: (args: Record<string, unknown>) => Promise<string>
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export const tools: Tool[] = [
|
|
17
|
-
{
|
|
18
|
-
name: 'read_file',
|
|
19
|
-
description: 'Read file contents',
|
|
20
|
-
params: '{"path": "string"}',
|
|
21
|
-
execute: async ({ path }) => {
|
|
22
|
-
try { return readFile(guardPath(path as string)) }
|
|
23
|
-
catch (e) { throw new Error(`read_file: ${e}`) }
|
|
24
|
-
},
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
name: 'list_files',
|
|
28
|
-
description: 'List directory contents',
|
|
29
|
-
params: '{"path": "string", "recursive": "boolean (optional)"}',
|
|
30
|
-
execute: async ({ path, recursive = false }) => {
|
|
31
|
-
const entries = listFiles(guardPath(path as string), recursive as boolean)
|
|
32
|
-
if (!entries.length) return '(empty)'
|
|
33
|
-
return entries.map(e => `${e.type === 'dir' ? 'd' : 'f'} ${e.rel}`).join('\n')
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
{
|
|
37
|
-
name: 'create_file',
|
|
38
|
-
description: 'Create a new file — fails if file already exists',
|
|
39
|
-
params: '{"path": "string", "content": "string"}',
|
|
40
|
-
execute: async ({ path, content }) => {
|
|
41
|
-
const safe = guardPath(path as string)
|
|
42
|
-
if (existsSync(safe)) throw new Error(`file already exists: ${path}`)
|
|
43
|
-
writeFile(safe, content as string)
|
|
44
|
-
return `created: ${path}`
|
|
45
|
-
},
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
name: 'edit_file',
|
|
49
|
-
description: 'Overwrite entire file — use only for new files or full rewrites',
|
|
50
|
-
params: '{"path": "string", "content": "string"}',
|
|
51
|
-
execute: async ({ path, content }) => {
|
|
52
|
-
writeFile(guardPath(path as string), content as string)
|
|
53
|
-
return `written: ${path}`
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
name: 'patch_file',
|
|
58
|
-
description: 'Replace an exact string in a file — use for targeted edits to existing files',
|
|
59
|
-
params: '{"path": "string", "old": "string", "new": "string"}',
|
|
60
|
-
execute: async ({ path, old: oldStr, new: newStr }) => {
|
|
61
|
-
const safe = guardPath(path as string)
|
|
62
|
-
const current = readFile(safe)
|
|
63
|
-
if (!current) throw new Error(`file not found or empty: ${path}`)
|
|
64
|
-
if (!current.includes(oldStr as string)) throw new Error(`old text not found in ${path}`)
|
|
65
|
-
const updated = current.replace(oldStr as string, newStr as string)
|
|
66
|
-
writeFile(safe, updated)
|
|
67
|
-
return `patched: ${path}`
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
{
|
|
71
|
-
name: 'delete_file',
|
|
72
|
-
description: 'Delete a file',
|
|
73
|
-
params: '{"path": "string"}',
|
|
74
|
-
execute: async ({ path }) => {
|
|
75
|
-
deleteFile(guardPath(path as string))
|
|
76
|
-
return `deleted: ${path}`
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: 'run_command',
|
|
81
|
-
description: 'Run a shell command in cwd',
|
|
82
|
-
params: '{"command": "string"}',
|
|
83
|
-
execute: async ({ command }) => {
|
|
84
|
-
const { stdout, stderr } = await run(command as string, { cwd: process.cwd(), timeout: EXEC_TIMEOUT_MS })
|
|
85
|
-
return [stdout, stderr ? `stderr: ${stderr}` : ''].filter(Boolean).join('\n').trim()
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
name: 'create_folder',
|
|
90
|
-
description: 'Create a directory (and any missing parents)',
|
|
91
|
-
params: '{"path": "string"}',
|
|
92
|
-
execute: async ({ path }) => {
|
|
93
|
-
createDir(guardPath(path as string))
|
|
94
|
-
return `created: ${path}`
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: 'move_file',
|
|
99
|
-
description: 'Move or rename a file or directory',
|
|
100
|
-
params: '{"from": "string", "to": "string"}',
|
|
101
|
-
execute: async ({ from, to }) => {
|
|
102
|
-
moveFile(guardPath(from as string), guardPath(to as string))
|
|
103
|
-
return `moved: ${from} → ${to}`
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
]
|
|
107
|
-
|
|
108
|
-
export function getSystemPrompt(extra = ''): string {
|
|
109
|
-
const toolDocs = tools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n')
|
|
110
|
-
return `You are Miii — a fast, local AI coding assistant.
|
|
111
|
-
|
|
112
|
-
Use tools by emitting:
|
|
113
|
-
<tool_call>
|
|
114
|
-
{"name": "tool_name", "args": {...}}
|
|
115
|
-
</tool_call>
|
|
116
|
-
|
|
117
|
-
Put file content in named blocks (never inside JSON — avoids escaping errors):
|
|
118
|
-
|
|
119
|
-
For edit_file / create_file use <content> block:
|
|
120
|
-
<tool_call>
|
|
121
|
-
{"name": "edit_file", "args": {"path": "src/foo.ts"}}
|
|
122
|
-
<content>
|
|
123
|
-
full file content here
|
|
124
|
-
</content>
|
|
125
|
-
</tool_call>
|
|
126
|
-
|
|
127
|
-
For patch_file use <old> and <new> blocks:
|
|
128
|
-
<tool_call>
|
|
129
|
-
{"name": "patch_file", "args": {"path": "src/foo.ts"}}
|
|
130
|
-
<old>
|
|
131
|
-
exact text to replace
|
|
132
|
-
</old>
|
|
133
|
-
<new>
|
|
134
|
-
replacement text
|
|
135
|
-
</new>
|
|
136
|
-
</tool_call>
|
|
137
|
-
|
|
138
|
-
Tools:
|
|
139
|
-
${toolDocs}
|
|
140
|
-
|
|
141
|
-
Rules:
|
|
142
|
-
- To modify an existing file: use patch_file with the exact old text and new replacement — do NOT rewrite the whole file
|
|
143
|
-
- To create a new file: use edit_file with full content in the <content> block
|
|
144
|
-
- read_file before patch_file so you know the exact text to match
|
|
145
|
-
- Never delete without confirming
|
|
146
|
-
- Be concise
|
|
147
|
-
- Output plain text only — never use markdown formatting in your responses
|
|
148
|
-
- No headers (no #, ##), no bold (**text**), no italic (*text*), no bullet points with *, no horizontal rules (---)
|
|
149
|
-
- No fenced code blocks with backticks in prose
|
|
150
|
-
- Use plain indentation and labels for structure. This is a terminal, not a chat UI${extra}`
|
|
151
|
-
}
|