miii-cli 0.2.0 → 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/README.md CHANGED
@@ -107,13 +107,36 @@ The model can call these tools automatically — no setup needed.
107
107
  | `edit_file` | Create or overwrite a file (auto-creates parent dirs) |
108
108
  | `create_folder` | Create a directory and any missing parents |
109
109
  | `move_file` | Move or rename a file or directory |
110
- | `delete_file` | Delete a file |
110
+ | `delete_file` | Delete a file (requires confirmation) |
111
111
  | `run_command` | Run a shell command in the current directory |
112
112
 
113
113
  Tool calls chain up to 6 hops deep — the model reads, edits, runs, and verifies on its own.
114
114
 
115
115
  ---
116
116
 
117
+ ## Thinking indicator
118
+
119
+ While miii processes your request, a rotating sparkle icon and sarcastic phrase appear so you always know something's happening:
120
+
121
+ ```
122
+ miii
123
+ ✦ staring into the abyss (it blinked)…
124
+ ```
125
+
126
+ The phrase rotates every 5 seconds. When a tool call is in flight, the display updates to show exactly which tool is running:
127
+
128
+ ```
129
+ miii
130
+ ⚙ running read_file…
131
+
132
+ miii
133
+ ⚙ running run_command…
134
+ ```
135
+
136
+ Each tool in a chain updates live as execution moves through them.
137
+
138
+ ---
139
+
117
140
  ## Sessions
118
141
 
119
142
  Every conversation is saved and resumed automatically.
@@ -170,8 +193,9 @@ Config is loaded from (in order):
170
193
  ```json
171
194
  {
172
195
  "model": "gpt-4o",
173
- "provider": "openai",
174
- "baseUrl": "https://api.openai.com/v1"
196
+ "provider": "openai-compat",
197
+ "baseUrl": "https://api.openai.com/v1",
198
+ "apiKey": "sk-..."
175
199
  }
176
200
  ```
177
201
 
@@ -184,27 +208,38 @@ Works with LM Studio, vLLM, Groq, Together, and any other OpenAI-compatible serv
184
208
  | Key | Action |
185
209
  |---|---|
186
210
  | `enter` | Send message |
187
- | `ctrl+c` | Abort streaming response |
211
+ | `ctrl+c` | Abort current request |
188
212
  | `ctrl+c` x2 | Exit miii |
189
- | `esc` | Close overlay or abort |
213
+ | `esc` | Close overlay or cancel |
190
214
  | `↑ / ↓` | Navigate command palette or file picker |
191
215
 
192
216
  ---
193
217
 
194
218
  ## Security
195
219
 
196
- miii **0.1.5** addresses the following OWASP issues:
220
+ miii **0.1.5+** addresses the following OWASP issues:
197
221
 
198
222
  | Issue | Fix |
199
223
  |---|---|
200
- | Path traversal (A01) | All file tool operations are now restricted to the current working directory via `guardPath()` |
224
+ | Path traversal (A01) | All file tool operations restricted to cwd via `guardPath()` |
201
225
  | Path traversal (A01) | `@filename` references validated against `cwd` before reading |
202
226
  | Path traversal (A01) | `/mv`, `/mkdir`, `/touch` commands restricted to `cwd` |
203
227
  | Path traversal (A01) | Session names sanitized to alphanumeric + hyphens only |
204
228
  | Injection (A03) | `run_command` tool enforces a 30-second execution timeout |
205
229
  | Insecure deserialization (A08) | Config loading whitelists allowed keys; session data validated as array |
206
230
  | XML injection | File paths in context XML attributes are properly escaped |
207
- | Configurable API key | OpenAI-compatible provider token now configurable via `apiKey` in config (no longer hardcoded) |
231
+ | Configurable API key | OpenAI-compatible provider token configurable via `apiKey` in config |
232
+
233
+ `delete_file` requires explicit user confirmation (`y/n`) before executing.
234
+
235
+ ---
236
+
237
+ ## What's new in 0.2.0
238
+
239
+ - **No streaming** — responses delivered in full, no partial renders or flickering
240
+ - **Thinking indicator** — rotating sarcastic phrases + sparkle icon while model processes
241
+ - **Tool status** — live indicator when a tool call is in flight
242
+ - **Status renamed** — internal status `streaming` → `thinking` throughout
208
243
 
209
244
  ---
210
245
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Claude Code-level terminal workflows powered by your local models.",
6
6
  "license": "MIT",
@@ -24,6 +24,11 @@
24
24
  "terminal",
25
25
  "local-ai"
26
26
  ],
27
+ "files": [
28
+ "dist",
29
+ "README.md",
30
+ "LICENSE"
31
+ ],
27
32
  "bin": {
28
33
  "miii": "dist/index.js"
29
34
  },
@@ -1,28 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(git -C /Users/akshay/Desktop/essentials/akshay-workspace/miii-cli status)",
5
- "Bash(npm install *)",
6
- "Bash(npx tsc *)",
7
- "Bash(node --input-type=module)",
8
- "Bash(chmod +x dist/index.js)",
9
- "Bash(node dist/index.js --help)",
10
- "Bash(node dist/index.js)",
11
- "Bash(chmod +x install.sh)",
12
- "Bash(npm whoami *)",
13
- "Bash(npm run *)",
14
- "Bash(npm publish *)",
15
- "Bash(npm pkg *)",
16
- "Bash(npm version *)",
17
- "Bash(chmod +x *)",
18
- "Bash(git grep *)",
19
- "Bash(npm link *)",
20
- "Bash(node -e \"const {appendFileSync} = require\\('fs'\\); appendFileSync\\('/tmp/miii-test.log', 'test\\\\n'\\); console.log\\('ok'\\)\")",
21
- "Read(//tmp/**)",
22
- "Read(//private/tmp/**)",
23
- "Read(//Users/akshay/**)",
24
- "Bash(tsx --version)",
25
- "Bash(node -e ' *)"
26
- ]
27
- }
28
- }
package/CONTRIBUTING.md DELETED
@@ -1,55 +0,0 @@
1
- # Contributing to miii-cli
2
-
3
- We welcome contributions! miii-cli is a community-driven project, and your help is invaluable whether you're fixing a bug, adding a feature, or improving documentation.
4
-
5
- ---
6
-
7
- ### 🚀 How to Contribute
8
-
9
- 1. **Fork** the repository.
10
- 2. **Clone** your fork locally.
11
- 3. **Create a new branch** (`git checkout -b feature/my-awesome-feature`).
12
- 4. **Commit** your changes and **Push** to the branch.
13
- 5. **Open a Pull Request (PR)** against the `main` branch.
14
-
15
- ### 🤝 Guidelines
16
-
17
- * **Code Style:** Please follow standard JavaScript/TypeScript best practices.
18
- * **Testing:** All new features must include accompanying unit or integration tests.
19
- * **Documentation:** If you change an API or add complex logic, please update the relevant documentation files.
20
-
21
- ### 📝 Workflow
22
-
23
- * **Bugs:** If you find a bug, please report it on the Issues page with a clear reproduction guide and expected behavior.
24
- * **Features:** Before starting a large feature, please open an Issue to discuss the scope and design with the core team.
25
-
26
- ### 🐛 Bug Reporting
27
-
28
- When reporting a bug, please include:
29
-
30
- 1. **Title:** A concise summary of the issue.
31
- 2. **Environment:** (e.g., Node.js version, OS, browser)
32
- 3. **Steps to Reproduce:** A numbered list of exact steps to trigger the bug.
33
- 4. **Expected Result:** What should have happened.
34
- 5. **Actual Result:** What actually happened.
35
-
36
- ***
37
-
38
- ### 🛠 Development Setup
39
-
40
- To work on miii-cli locally, please ensure you have the necessary dependencies installed.
41
-
42
- 1. **Install Dependencies:**
43
- `npm install`
44
-
45
- 2. **Common Commands:**
46
- We use a Makefile to streamline common tasks:
47
-
48
- Development/Live Run: `make dev` (Runs the application in development mode)
49
- Build Project: `make build` (Compiles the TypeScript source code)
50
- Install Globally: `make install` (Links the project using `npm link`)
51
- Clean Build: `make clean` (Removes compiled output)
52
-
53
- ### 🧪 Testing
54
-
55
- When writing tests, please ensure they cover both the happy path and expected edge cases.
package/Makefile DELETED
@@ -1,13 +0,0 @@
1
- .PHONY: dev build install clean
2
-
3
- dev:
4
- tsx src/index.ts
5
-
6
- build:
7
- tsc
8
-
9
- install: build
10
- npm link
11
-
12
- clean:
13
- rm -rf dist
package/install.sh DELETED
@@ -1,6 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
- npm install
4
- npm run build
5
- npm link
6
- echo "miii installed. Run: miii"
package/mii-cli.gif DELETED
Binary file
package/src/config.ts DELETED
@@ -1,32 +0,0 @@
1
- import { readFileSync, existsSync } from 'fs'
2
- import { homedir } from 'os'
3
- import { join } from 'path'
4
- import type { Config } from './types.js'
5
-
6
- const defaults: Config = {
7
- model: 'llama3.2',
8
- provider: 'ollama',
9
- baseUrl: 'http://localhost:11434',
10
- }
11
-
12
- const ALLOWED_KEYS = new Set<keyof Config>(['model', 'provider', 'baseUrl', 'systemPrompt', 'apiKey'])
13
-
14
- export function loadConfig(): Config {
15
- const candidates = [
16
- join(process.cwd(), '.miii.json'),
17
- join(homedir(), '.config', 'miii', 'config.json'),
18
- ]
19
- for (const p of candidates) {
20
- if (existsSync(p)) {
21
- try {
22
- const raw: Record<string, unknown> = JSON.parse(readFileSync(p, 'utf-8'))
23
- const safe: Partial<Config> = {}
24
- for (const key of ALLOWED_KEYS) {
25
- if (key in raw) (safe as Record<string, unknown>)[key] = raw[key]
26
- }
27
- return { ...defaults, ...safe }
28
- } catch {}
29
- }
30
- }
31
- return { ...defaults }
32
- }
package/src/files/ops.ts DELETED
@@ -1,89 +0,0 @@
1
- import {
2
- readFileSync, writeFileSync, unlinkSync,
3
- mkdirSync, readdirSync, statSync, existsSync, renameSync,
4
- } from 'fs'
5
- import { join, dirname, relative, extname, resolve, sep } from 'path'
6
-
7
- export function guardPath(p: string, base = process.cwd()): string {
8
- const abs = resolve(base, p)
9
- const root = resolve(base)
10
- if (abs !== root && !abs.startsWith(root + sep)) {
11
- throw new Error(`path outside working directory: ${p}`)
12
- }
13
- return abs
14
- }
15
-
16
- const SKIP_DIRS = new Set([
17
- 'node_modules', 'dist', 'build', '.git', '.next', '.nuxt', '.svelte-kit',
18
- 'out', '__pycache__', '.cache', 'coverage', '.nyc_output', 'vendor',
19
- 'target', '.turbo', '.vercel', 'generated', '.gradle', '.expo',
20
- 'bin', 'obj', '.idea', '.vscode', 'tmp', 'temp', 'logs',
21
- ])
22
-
23
- const SKIP_EXTS = new Set([
24
- '.map', '.lock',
25
- '.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.bmp', '.tiff',
26
- '.mp4', '.mp3', '.wav', '.ogg', '.pdf',
27
- '.zip', '.tar', '.gz', '.rar', '.7z',
28
- '.exe', '.dll', '.so', '.dylib', '.wasm', '.class', '.pyc',
29
- '.ttf', '.woff', '.woff2', '.eot',
30
- ])
31
-
32
- const SKIP_NAMES = new Set([
33
- 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Cargo.lock',
34
- 'poetry.lock', 'Gemfile.lock', 'composer.lock',
35
- '.DS_Store', 'Thumbs.db', '.env.local', 'LICENSE', 'LICENSE.md',
36
- ])
37
-
38
- export function readFile(p: string): string {
39
- if (!existsSync(p)) return ''
40
- return readFileSync(p, 'utf-8')
41
- }
42
-
43
- export function writeFile(p: string, content: string): void {
44
- mkdirSync(dirname(p), { recursive: true })
45
- writeFileSync(p, content, 'utf-8')
46
- }
47
-
48
- export function deleteFile(p: string): void {
49
- unlinkSync(p)
50
- }
51
-
52
- export function createDir(p: string): void {
53
- mkdirSync(p, { recursive: true })
54
- }
55
-
56
- export function moveFile(from: string, to: string): void {
57
- mkdirSync(dirname(to), { recursive: true })
58
- renameSync(from, to)
59
- }
60
-
61
- export interface FileEntry {
62
- name: string
63
- path: string
64
- rel: string
65
- type: 'file' | 'dir'
66
- size?: number
67
- }
68
-
69
- export function listFiles(dir: string, recursive = false, cwd = process.cwd()): FileEntry[] {
70
- if (!existsSync(dir)) return []
71
- const entries: FileEntry[] = []
72
- for (const name of readdirSync(dir)) {
73
- if (name.startsWith('.')) continue
74
- if (SKIP_NAMES.has(name)) continue
75
- if (SKIP_DIRS.has(name)) continue
76
- const ext = extname(name)
77
- if (SKIP_EXTS.has(ext)) continue
78
- if (name.endsWith('.d.ts') || name.endsWith('.js.map')) continue
79
- let stat
80
- try { stat = statSync(join(dir, name)) } catch { continue }
81
- const full = join(dir, name)
82
- const type = stat.isDirectory() ? 'dir' : 'file'
83
- entries.push({ name, path: full, rel: relative(cwd, full), type, size: stat.isFile() ? stat.size : undefined })
84
- if (recursive && type === 'dir') {
85
- entries.push(...listFiles(full, true, cwd))
86
- }
87
- }
88
- return entries
89
- }
package/src/index.ts DELETED
@@ -1,11 +0,0 @@
1
- #!/usr/bin/env node
2
- // Minimal entry — heavy imports lazy-loaded
3
- async function main() {
4
- const { lazyInit } = await import('./init.js')
5
- await lazyInit()
6
- }
7
-
8
- main().catch(err => {
9
- process.stderr.write(`fatal: ${err.message}\n`)
10
- process.exit(1)
11
- })
package/src/init.ts DELETED
@@ -1,41 +0,0 @@
1
- import { render } from 'ink'
2
- import React from 'react'
3
- import minimist from 'minimist'
4
- import { loadConfig } from './config.js'
5
- import { SkillLoader } from './skills/loader.js'
6
- import { InputBar } from './tui/InputBar.js'
7
- import { welcome } from './tui/printer.js'
8
- import { ensureOllama } from './llm/ollama.js'
9
-
10
- export async function lazyInit(): Promise<void> {
11
- const argv = minimist(process.argv.slice(2), {
12
- string: ['model', 'url', 'provider', 'session'],
13
- alias: { m: 'model', u: 'url', p: 'provider', s: 'session' },
14
- })
15
-
16
- const config = loadConfig()
17
- if (argv.model) config.model = argv.model
18
- if (argv.url) config.baseUrl = argv.url
19
- if (argv.provider) config.provider = argv.provider as typeof config.provider
20
-
21
- if (config.provider === 'ollama') {
22
- await ensureOllama(config.baseUrl)
23
- }
24
-
25
- const skills = new SkillLoader()
26
- await skills.loadAll()
27
-
28
- // Print welcome banner to scrollback BEFORE Ink starts
29
- welcome(config.provider, config.model, process.cwd())
30
-
31
- // Ink renders ONLY the input bar (small footprint at bottom)
32
- // patchConsole: true (default) ensures console.log output appears above Ink
33
- const sessionName = (argv.session as string) || 'default'
34
-
35
- const { waitUntilExit } = render(
36
- React.createElement(InputBar, { config, skills, cwd: process.cwd(), session: sessionName }),
37
- { exitOnCtrlC: false }
38
- )
39
-
40
- await waitUntilExit()
41
- }
package/src/llm/ollama.ts DELETED
@@ -1,110 +0,0 @@
1
- import { execSync, spawn } from 'child_process'
2
-
3
- export interface OllamaModel {
4
- name: string
5
- size: number
6
- modified_at: string
7
- digest?: string
8
- }
9
-
10
- export async function listModels(baseUrl: string): Promise<OllamaModel[]> {
11
- const res = await fetch(`${baseUrl}/api/tags`)
12
- if (!res.ok) throw new Error(`Ollama ${res.status}: ${await res.text()}`)
13
- const data = (await res.json()) as { models?: OllamaModel[] }
14
- return data.models ?? []
15
- }
16
-
17
- export async function pullModel(
18
- baseUrl: string,
19
- name: string,
20
- onProgress: (status: string, pct: number | undefined) => void,
21
- signal?: AbortSignal,
22
- ): Promise<void> {
23
- const res = await fetch(`${baseUrl}/api/pull`, {
24
- method: 'POST',
25
- headers: { 'Content-Type': 'application/json' },
26
- body: JSON.stringify({ name, stream: true }),
27
- signal,
28
- })
29
- if (!res.ok) throw new Error(`pull failed: ${res.status} ${await res.text()}`)
30
-
31
- const reader = res.body!.getReader()
32
- const dec = new TextDecoder()
33
- let buf = ''
34
-
35
- try {
36
- while (true) {
37
- const { done, value } = await reader.read()
38
- if (done) break
39
- buf += dec.decode(value, { stream: true })
40
- const lines = buf.split('\n')
41
- buf = lines.pop() ?? ''
42
- for (const line of lines) {
43
- if (!line.trim()) continue
44
- try {
45
- const obj = JSON.parse(line) as { status?: string; completed?: number; total?: number }
46
- const pct = obj.total ? Math.round(((obj.completed ?? 0) / obj.total) * 100) : undefined
47
- onProgress(obj.status ?? '', pct)
48
- } catch {}
49
- }
50
- }
51
- } finally {
52
- reader.releaseLock()
53
- }
54
- }
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
-
106
- export function fmtSize(bytes: number): string {
107
- if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)}GB`
108
- if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(0)}MB`
109
- return `${(bytes / 1e3).toFixed(0)}KB`
110
- }
package/src/llm/stream.ts DELETED
@@ -1,55 +0,0 @@
1
- import type { ChatMessage } from '../types.js'
2
-
3
- export interface ChatConfig {
4
- provider: 'ollama' | 'openai-compat'
5
- model: string
6
- baseUrl: string
7
- apiKey?: string
8
- messages: ChatMessage[]
9
- signal?: AbortSignal
10
- onDone: (fullText: string) => void | Promise<void>
11
- onError: (err: Error) => void
12
- }
13
-
14
- export async function chat(cfg: ChatConfig): Promise<void> {
15
- if (cfg.provider === 'openai-compat') return chatOpenAI(cfg)
16
- return chatOllama(cfg)
17
- }
18
-
19
- async function chatOllama(cfg: ChatConfig): Promise<void> {
20
- const { model, messages, baseUrl, signal, onDone, onError } = cfg
21
- try {
22
- const res = await fetch(`${baseUrl}/api/chat`, {
23
- method: 'POST',
24
- headers: { 'Content-Type': 'application/json' },
25
- body: JSON.stringify({ model, messages, stream: false }),
26
- signal,
27
- })
28
- if (!res.ok) { onError(new Error(`Ollama ${res.status}: ${await res.text()}`)); return }
29
- const obj = await res.json()
30
- await onDone(obj?.message?.content ?? '')
31
- } catch (err) {
32
- if ((err as Error)?.name !== 'AbortError') onError(toError(err))
33
- }
34
- }
35
-
36
- async function chatOpenAI(cfg: ChatConfig): Promise<void> {
37
- const { model, messages, baseUrl, apiKey, signal, onDone, onError } = cfg
38
- try {
39
- const res = await fetch(`${baseUrl}/v1/chat/completions`, {
40
- method: 'POST',
41
- headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${apiKey ?? 'local'}` },
42
- body: JSON.stringify({ model, messages }),
43
- signal,
44
- })
45
- if (!res.ok) { onError(new Error(`LLM ${res.status}: ${await res.text()}`)); return }
46
- const obj = await res.json()
47
- await onDone(obj?.choices?.[0]?.message?.content ?? '')
48
- } catch (err) {
49
- if ((err as Error)?.name !== 'AbortError') onError(toError(err))
50
- }
51
- }
52
-
53
- function toError(e: unknown): Error {
54
- return e instanceof Error ? e : new Error(String(e))
55
- }