miii-cli 0.3.0 β†’ 0.3.1

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
@@ -9,87 +9,90 @@
9
9
  [![license](https://img.shields.io/npm/l/miii-cli)](LICENSE)
10
10
  [![node](https://img.shields.io/node/v/miii-cli)](https://nodejs.org)
11
11
 
12
- ## πŸ“Š How Miii Stacks Up
12
+ ## πŸ“Š The Competitive Edge
13
13
 
14
14
  | Feature | **Miii** | Claude Code | Codex CLI | Aider |
15
15
  |---|---|---|---|---|
16
- | **Runs locally** | βœ… Ollama / any API | ❌ Cloud only | ❌ Cloud only | βœ… Local + cloud |
17
- | **Code stays private** | βœ… Never leaves machine | ❌ Sent to Anthropic | ❌ Sent to OpenAI | ⚠️ Depends on model |
18
- | **Cost** | πŸ†“ Free (your compute) | πŸ’³ Pay per token | πŸ’³ Pay per token | πŸ†“ Free (local) |
19
- | **Runtime** | ⚑ TypeScript β€” instant start | 🐍 Node (fast) | 🐍 Node | 🐒 Python |
20
- | **Deep Think mode** | βœ… Gather + synthesize | ❌ | ❌ | ❌ |
21
- | **Auto-test loop** | βœ… Jest / Vitest / Mocha | ⚠️ Manual | ❌ | ⚠️ Manual |
22
- | **Web search built-in** | βœ… Tavily | ❌ | ❌ | ❌ |
23
- | **Surgical patch edits** | βœ… `patch_file` | βœ… | ⚠️ | βœ… |
24
- | **Session memory** | βœ… Named, persistent | βœ… | ❌ | ⚠️ Basic |
25
- | **Skill / plugin system** | βœ… npm + `.md` skills | ⚠️ MCP only | ❌ | ❌ |
26
- | **Open source** | βœ… MIT | ❌ | ❌ | βœ… Apache 2.0 |
27
-
28
- > βœ… = supported  |  ⚠️ = partial  |  ❌ = not supported
16
+ | **Execution Environment** | βœ… Local / Hybrid | ❌ Cloud only | ❌ Cloud only | βœ… Local + cloud |
17
+ | **Data Privacy** | βœ… Air-gapped possible | ❌ Cloud-streamed | ❌ Cloud-streamed | ⚠️ Model-dependent |
18
+ | **Cost Structure** | πŸ†“ Free (Your Compute) | πŸ’³ Token-based | πŸ’³ Token-based | πŸ†“ Free (local) |
19
+ | **Runtime Efficiency** | ⚑ TS (Instant Start) | 🐍 Node (Fast) | 🐍 Node | 🐒 Python (Heavy) |
20
+ | **Research Engine** | βœ… Deep Think Mode | ❌ | ❌ | ❌ |
21
+ | **Validation Loop** | βœ… Auto-test (Jest/Vitest) | ⚠️ Manual | ❌ | ⚠️ Manual |
22
+ | **Live Web Access** | βœ… Tavily Integrated | ❌ | ❌ | ❌ |
23
+ | **Edit Precision** | βœ… Surgical `patch_file` | βœ… | ⚠️ | βœ… |
24
+ | **State Persistence** | βœ… Named Sessions | βœ… | ❌ | ⚠️ Basic |
25
+ | **Extensibility** | βœ… npm + `.md` Skills | ⚠️ MCP only | ❌ | ❌ |
26
+ | **License** | βœ… MIT | ❌ | ❌ | βœ… Apache 2.0 |
27
+
28
+ > βœ… = Native  |  ⚠️ = Partial  |  ❌ = Unsupported
29
29
 
30
30
  ## ⚑️ Quick Start
31
31
 
32
- Get up and running in 30 seconds:
32
+ Deploy Miii in your environment in 30 seconds:
33
33
 
34
34
  ```bash
35
+ # 1. Pull a capable local model
35
36
  ollama pull qwen2.5-coder:7b
37
+
38
+ # 2. Install the CLI globally
36
39
  npm install -g miii-cli
40
+
41
+ # 3. Start engineering
37
42
  miii
38
43
  ```
39
44
 
40
45
  ## 🧠 Why Miii?
41
46
 
42
- Most AI coding tools are either heavy Python wrappers or expensive monthly subscriptions that send your code to the cloud. **miii is different.**
47
+ The industry is saturated with heavy Python wrappers and expensive monthly subscriptions that trade your intellectual property for convenience. **Miii breaks this cycle.**
43
48
 
44
- - **Local-First & Private**: Runs on Ollama or any OpenAI-compatible API. Your code never leaves your machine, ensuring 100% privacy and security.
45
- - **Blazing Fast**: Built with TypeScript for near-instant startup. No heavy Python runtime overhead. Tiny footprint, massive power.
46
- - **Fully Autonomous**: Miii doesn't just suggest code; it acts as a junior engineerβ€”editing files, running your test suite, and iterating until the bugs are gone.
47
- - **Deep Context Awareness**: Automatically analyzes git diffs and project architecture, eliminating the need for manual copy-pasting.
49
+ - **Privacy by Default**: Your codebase never leaves your machine. Period.
50
+ - **Zero Latency**: Built with TypeScript for near-instant startup. No virtual environments, no dependency hell, just raw performance.
51
+ - **True Autonomy**: Miii isn't a chatbot; it's a junior engineer. It plans, edits, runs tests, and iterates until the PR is ready.
52
+ - **Architectural Intelligence**: By analyzing git diffs and project structure, Miii understands context without requiring manual copy-pasting.
48
53
 
49
54
  ## πŸ”₯ Killer Features
50
55
 
51
- - **πŸ›  Precision Editing**: Using `patch_file`, miii makes surgical changes without rewriting entire files.
52
- - **πŸ”„ Auto-Test Loop**: Miii runs your Jest/Vitest/Mocha tests after every edit. If it breaks, it fixes itself.
53
- - **🌐 Web Intelligence**: Integrated `web_search` and `web_extract` via Tavily for real-time documentation.
54
- - **🧠 Deep Think**: Two-phase research mode β€” gathers from files, git, and web first, then synthesizes a complete answer. Available as `/think <query>` or as a tool the LLM calls autonomously.
55
- - **πŸ“ Planning Mode**: Use `/plan` to architect a solution before a single line of code is written.
56
- - **πŸ“‚ Session Memory**: Every conversation is auto-named and persisted. Resume your work instantly with `miii --session feature-auth`.
57
- - **πŸ“¦ Skill System**: Extend miii with npm skill plugins or custom `.md` files.
56
+ - **πŸ›  Surgical Precision**: Instead of overwriting files, Miii uses `patch_file` to inject changes, preserving your formatting and reducing token waste.
57
+ - **πŸ”„ The Self-Healing Loop**: Miii executes your test suite (Jest, Vitest, Mocha) after every change. If a test fails, it analyzes the trace and fixes the code autonomously.
58
+ - **🌐 Real-time Intelligence**: Integrated `web_search` and `web_extract` via Tavily allow Miii to reference the latest documentation and API changes.
59
+ - **🧠 Deep Think Engine**: A sophisticated two-phase research mode that gathers data before synthesizing a solution.
60
+ - **πŸ“ Strategic Planning**: Use `/plan` to map out complex refactors before a single character is typed.
61
+ - **πŸ“‚ Persistent Context**: Workflows are saved as named sessions. Jump back into a specific feature branch with `miii --session feature-auth`.
62
+ - **πŸ“¦ Modular Skill System**: Extend Miii's capabilities using npm plugins or simple Markdown-based skill files.
58
63
 
59
- ## 🧠 Deep Think
64
+ ## πŸ”¬ Deep Think Explained
60
65
 
61
- Deep think is a two-phase research engine built into miii:
66
+ Deep Think is a recursive research engine designed to eliminate "hallucinations" by grounding the AI in facts:
62
67
 
63
- 1. **Gather phase** β€” runs a constrained inner loop with read-only tools: `read_file`, `list_files`, `git_status`, `git_log`, `git_diff`, `web_search`, `web_extract`. Guardrails enforce a hard cap of 6 tool calls and 4 web calls. No file writes, no shell mutations.
64
- 2. **Synthesize phase** β€” gathered findings feed into the main run loop for a complete, grounded answer.
68
+ 1. **Gather Phase**: A constrained, read-only loop utilizing `read_file`, `list_files`, `git_status`, `git_log`, `git_diff`, and web tools.
69
+ - **Guardrails**: Strict limit of 6 tool calls and 4 web calls to prevent infinite loops.
70
+ - **Safety**: Zero write permissions. No mutations.
71
+ 2. **Synthesize Phase**: All gathered intelligence is aggregated and fed into the main execution loop for a grounded, verified response.
65
72
 
66
- **Two ways to trigger:**
67
-
68
- ```
69
- /think how does the auth middleware handle token expiry?
70
- /think what does this codebase do and how is it structured?
71
- /think latest breaking changes in react 19
73
+ **Trigger Research:**
74
+ ```bash
75
+ /think "How does the auth middleware handle token expiry?"
76
+ /think "Analyze the project structure and explain the data flow."
77
+ /think "What are the breaking changes in React 19 for this project?"
72
78
  ```
73
-
74
- The LLM can also call `deep_think` autonomously mid-conversation when it decides a question needs multi-source research before answering.
75
-
76
- > Requires a Tavily key (`/tavily-key tvly-...`) for web calls. File/git research works without it.
79
+ *The LLM also triggers `deep_think` autonomously when it detects a high-complexity query.*
77
80
 
78
81
  ## ⌨️ Command Cheat Sheet
79
82
 
80
- | Command | What it does |
81
- |---|---|
82
- | `/think <query>` | Deep research: gather from files + web, then synthesize answer |
83
- | `/refactor <goal>` | The powerhouse: plans, edits, and tests across your whole codebase |
84
- | `/git <sub>` | Instant git status, diffs, and automated commit messages |
85
- | `/plan` | Stop coding, start thinking (Structured Planning Mode) |
86
- | `/model <name>` | Swap LLMs on the fly |
87
- | `/tavily-key <key>` | Enable real-time web browsing |
88
- | `/sessions` | Travel back in time to previous coding sessions |
83
+ | Command | Purpose | Impact |
84
+ |---|---|---|
85
+ | `/think <query>` | Deep Research | High-fidelity synthesis of files + web |
86
+ | `/refactor <goal>` | Full-scale Engineering | Plan $\rightarrow$ Edit $\rightarrow$ Test loop |
87
+ | `/git <sub>` | Git Automation | Instant status, diffs, and AI commit messages |
88
+ | `/plan` | Architecture Mode | Structured blueprinting before coding |
89
+ | `/model <name>` | LLM Hot-swap | Switch models instantly based on task |
90
+ | `/tavily-key <key>` | Enable Web Access | Unlocks real-time internet browsing |
91
+ | `/sessions` | Context Recovery | Resume previous engineering sessions |
89
92
 
90
93
  ## βš™οΈ Configuration
91
94
 
92
- Customise your experience in `.miii.json` or `~/.config/miii/config.json`:
95
+ Fine-tune your agent in `.miii.json` or `~/.config/miii/config.json`:
93
96
 
94
97
  ```json
95
98
  {
@@ -110,13 +113,12 @@ cd miii-cli && npm install && npm run build && npm link
110
113
 
111
114
  ## 🌟 Community & Philosophy
112
115
 
113
- **Own your AI stack. Stop renting your intelligence. The future of coding is local.**
116
+ **Stop renting your intelligence. Own your AI stack.**
114
117
 
115
- miii is built for the community. If this tool saves you hours of coding, help us grow:
118
+ Miii is built for engineers who value privacy, speed, and total control. If this tool has accelerated your workflow, support the project:
116
119
  - 🌟 **Star the repo** on GitHub
117
- - 🐦 **Share on X**
118
- - πŸ€– **Post on Reddit**
119
- - πŸ’¬ **Tell a fellow developer**
120
+ - 🐦 **Share your wins** on X
121
+ - πŸ€– **Discuss the future** on Reddit
120
122
 
121
123
  ## πŸ“œ License
122
124
  MIT
@@ -8,6 +8,7 @@ import { tools } from '../tools/index.js';
8
8
  import { readFile } from '../files/ops.js';
9
9
  import { generateId } from '../types.js';
10
10
  import * as printer from './printer.js';
11
+ import { toolArgSummary } from './printer.js';
11
12
  import { loadSession, saveSession, listSessions, deleteSession } from '../sessions.js';
12
13
  import { MacroQueue, MicroQueue } from '../tasks/queue.js';
13
14
  import { TaskExecutor } from '../tasks/executor.js';
@@ -61,7 +62,7 @@ export function InputBar({ config, skills, cwd, session, version }) {
61
62
  },
62
63
  }), [config]);
63
64
  const allTools = useMemo(() => [...tools, deepThinkTool], [deepThinkTool]);
64
- const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, runLoop, handleAbort, } = useRunLoop(config, currentModelRef, pushHistory, allTools, abortRef);
65
+ const { status, setStatus, tick, currentTool, setCurrentTool, taskLabel, setTaskLabel, thinkingStartRef, runLoop, handleAbort, permissionRequest, resolvePermission, } = useRunLoop(config, currentModelRef, pushHistory, allTools, abortRef);
65
66
  // ─── refactor ─────────────────────────────────────────────────────────────
66
67
  const runRefactor = useCallback(async (goal) => {
67
68
  printer.systemMsg(`refactor: ${goal}`);
@@ -514,7 +515,7 @@ export function InputBar({ config, skills, cwd, session, version }) {
514
515
  }, [skills, runLoop, openPicker]);
515
516
  const skillList = skills.list();
516
517
  // ─── render ────────────────────────────────────────────────────────────────
517
- return (_jsxs(Box, { flexDirection: "column", children: [pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); } }), _jsx(Divider, { cols: cols })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: status === 'thinking'
518
+ return (_jsxs(Box, { flexDirection: "column", children: [pickerOpen ? (_jsxs(_Fragment, { children: [_jsx(ModelPicker, { models: pickerModels, current: currentModel, loading: pickerLoading, error: pickerError, pull: pullState, onSelect: handleModelSelect, onPull: handleModelPull, onClose: () => { setPickerOpen(false); } }), _jsx(Divider, { cols: cols })] })) : permissionRequest ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 0, children: _jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "yellow", children: "\u26A0" }), _jsx(Text, { color: "white", bold: true, children: permissionRequest.toolName }), _jsx(Text, { color: "gray", children: toolArgSummary(permissionRequest.args) })] }) }), _jsx(Divider, { cols: cols })] })) : (status === 'thinking' || status === 'tool') ? (_jsxs(_Fragment, { children: [_jsx(Box, { flexDirection: "column", paddingX: 1, children: _jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { children: status === 'thinking'
518
519
  ? _jsxs(_Fragment, { children: [_jsxs(Text, { color: "yellow", children: [SPARKLE[tick % SPARKLE.length], " "] }), _jsx(Text, { color: "gray", dimColor: true, italic: true, children: THINKING_PHRASES[phraseSeq[Math.floor(tick / 62) % phraseSeq.length]] })] })
519
- : _jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }) }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [Math.floor((Date.now() - thinkingStartRef.current) / 1000), "s"] }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })] }) }), _jsx(Divider, { cols: cols })] })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, onSubmit: handleSubmit, onAbort: handleAbort })] }));
520
+ : _jsxs(Text, { color: "yellow", dimColor: true, children: ["\u2699 running ", currentTool ?? 'tool', "\u2026"] }) }), _jsxs(Box, { gap: 2, children: [_jsxs(Text, { color: "gray", dimColor: true, children: [Math.floor((Date.now() - thinkingStartRef.current) / 1000), "s"] }), taskLabel && _jsx(Text, { color: "cyan", dimColor: true, children: taskLabel })] })] }) }), _jsx(Divider, { cols: cols })] })) : null, _jsx(InputArea, { status: status, skills: skillList, cwd: cwd, planningMode: planningMode, permissionRequest: permissionRequest, onPermissionResponse: resolvePermission, onSubmit: handleSubmit, onAbort: handleAbort })] }));
520
521
  }
@@ -34,7 +34,7 @@ const PLANNING_COMMANDS = [
34
34
  ];
35
35
  const PASTE_MIN_LINES = 3;
36
36
  const PASTE_MIN_CHARS = 200;
37
- export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort }) {
37
+ export function InputArea({ status, skills, cwd, planningMode, permissionRequest, onPermissionResponse, onSubmit, onAbort }) {
38
38
  const [lines, setLines] = useState(['']);
39
39
  const [cursor, setCursor] = useState({ row: 0, col: 0 });
40
40
  const [overlay, setOverlay] = useState('none');
@@ -148,6 +148,18 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
148
148
  setOverlayIdx(0);
149
149
  }
150
150
  useInput((input, key) => {
151
+ // Permission prompt intercepts all input
152
+ if (permissionRequest && onPermissionResponse) {
153
+ if (input === 'y' || input === 'Y') {
154
+ onPermissionResponse(true);
155
+ return;
156
+ }
157
+ if (input === 'n' || input === 'N' || key.escape) {
158
+ onPermissionResponse(false);
159
+ return;
160
+ }
161
+ return;
162
+ }
151
163
  // ESC: close overlay, abort stream, or clear input
152
164
  if (key.escape) {
153
165
  if (overlay !== 'none') {
@@ -296,19 +308,21 @@ export function InputArea({ status, skills, cwd, planningMode, onSubmit, onAbort
296
308
  }
297
309
  });
298
310
  const isProcessing = status !== 'idle';
299
- const borderColor = isProcessing ? 'yellow' : 'cyan';
300
- const hint = isProcessing
301
- ? 'esc to abort'
302
- : pasteLines > 0
303
- ? 'backspace removes paste enter to send'
304
- : overlay === 'command' && !commandQuery.includes(' ')
305
- ? '↑↓ navigate enter select esc close'
306
- : overlay === 'at'
311
+ const borderColor = permissionRequest ? 'yellow' : isProcessing ? 'yellow' : 'cyan';
312
+ const hint = permissionRequest
313
+ ? 'y approve n deny'
314
+ : isProcessing
315
+ ? 'esc to abort'
316
+ : pasteLines > 0
317
+ ? 'backspace removes paste enter to send'
318
+ : overlay === 'command' && !commandQuery.includes(' ')
307
319
  ? '↑↓ navigate enter select esc close'
308
- : planningMode
309
- ? 'πŸ“‹ planning mode / suggestions enter send /plan:done to exit'
310
- : '@ file / command enter send ctrl+c exit';
311
- return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsxs(Box, { borderStyle: "round", borderColor: borderColor, paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: borderColor, bold: true, children: '❯ ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: pasteLines > 0 ? (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "\u2398" }), _jsxs(Text, { color: "cyan", children: ["pasted ", pasteLines, " lines"] }), (lines.length > 1 || lines[0]) && (_jsx(Text, { color: "gray", dimColor: true, children: "+ typed text" }))] })) : lines.length === 1 && !lines[0] ? (_jsx(Text, { color: isActive ? 'white' : 'gray', dimColor: isProcessing, children: isActive ? 'β–ˆ' : 'processing...' })) : (lines.map((line, i) => (_jsx(Text, { wrap: "wrap", children: i === cursor.row
320
+ : overlay === 'at'
321
+ ? '↑↓ navigate enter select esc close'
322
+ : planningMode
323
+ ? 'πŸ“‹ planning mode / suggestions enter send /plan:done to exit'
324
+ : '@ file / command enter send ctrl+c exit';
325
+ return (_jsxs(Box, { flexDirection: "column", children: [overlay === 'command' && (_jsx(CommandPalette, { skills: allCommands, query: commandQuery, idx: overlayIdx })), overlay === 'at' && (_jsx(AtPicker, { files: filteredFiles, query: atQuery, idx: overlayIdx })), _jsxs(Box, { borderStyle: "round", borderColor: borderColor, paddingX: 1, flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: borderColor, bold: true, children: '❯ ' }), _jsx(Box, { flexDirection: "column", flexGrow: 1, children: permissionRequest ? (_jsxs(Box, { gap: 2, children: [_jsx(Text, { color: "green", bold: true, children: "y yes" }), _jsx(Text, { color: "red", bold: true, children: "n no" })] })) : pasteLines > 0 ? (_jsxs(Box, { gap: 1, children: [_jsx(Text, { color: "cyan", children: "\u2398" }), _jsxs(Text, { color: "cyan", children: ["pasted ", pasteLines, " lines"] }), (lines.length > 1 || lines[0]) && (_jsx(Text, { color: "gray", dimColor: true, children: "+ typed text" }))] })) : lines.length === 1 && !lines[0] ? (_jsx(Text, { color: isActive ? 'white' : 'gray', dimColor: isProcessing, children: isActive ? 'β–ˆ' : 'processing...' })) : (lines.map((line, i) => (_jsx(Text, { wrap: "wrap", children: i === cursor.row
312
326
  ? renderLineWithCursor(line, cursor.col, isActive)
313
327
  : line }, i)))) })] }), _jsx(Text, { color: "gray", dimColor: true, children: hint })] })] }));
314
328
  }
@@ -7,16 +7,24 @@ import * as printer from '../printer.js';
7
7
  const MAX_TOOL_DEPTH = 6;
8
8
  const FILE_EDIT_TOOLS = new Set(['edit_file', 'create_file', 'patch_file', 'delete_file']);
9
9
  const SHOW_RESULT_TOOLS = new Set(['run_tests', 'git_commit']);
10
+ const PERMISSION_TOOLS = new Set(['edit_file', 'patch_file', 'delete_file', 'create_file', 'move_file', 'run_command', 'git_commit']);
10
11
  export function useRunLoop(config, currentModelRef, pushHistory, extraTools = [], abortRef) {
11
12
  const [status, setStatus] = useState('idle');
12
13
  const [tick, setTick] = useState(0);
13
14
  const [currentTool, setCurrentTool] = useState();
14
15
  const [taskLabel, setTaskLabel] = useState();
16
+ const [permissionRequest, setPermissionRequest] = useState(null);
17
+ const permissionResolveRef = useRef(null);
15
18
  const thinkingStartRef = useRef(0);
16
19
  const extraToolsRef = useRef(extraTools);
17
20
  extraToolsRef.current = extraTools;
18
21
  const pushHistoryRef = useRef(pushHistory);
19
22
  useEffect(() => { pushHistoryRef.current = pushHistory; }, [pushHistory]);
23
+ const resolvePermission = useCallback((approved) => {
24
+ permissionResolveRef.current?.(approved);
25
+ permissionResolveRef.current = null;
26
+ setPermissionRequest(null);
27
+ }, []);
20
28
  useEffect(() => {
21
29
  if (status === 'idle')
22
30
  return;
@@ -78,6 +86,17 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
78
86
  const allTools = [...staticTools, ...extraToolsRef.current];
79
87
  const tool = allTools.find(t => t.name === tc.name);
80
88
  setCurrentTool(tc.name);
89
+ if (PERMISSION_TOOLS.has(tc.name)) {
90
+ const approved = await new Promise(resolve => {
91
+ permissionResolveRef.current = resolve;
92
+ setPermissionRequest({ toolName: tc.name, args: tc.args });
93
+ });
94
+ if (!approved) {
95
+ printer.systemMsg(`denied: ${tc.name}`);
96
+ next.push({ role: 'user', content: `Tool ${tc.name} was denied by the user` });
97
+ break;
98
+ }
99
+ }
81
100
  if (tool) {
82
101
  try {
83
102
  printer.toolCallStart(tc.name, tc.args);
@@ -136,6 +155,11 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
136
155
  }, [config]);
137
156
  const handleAbort = useCallback(() => {
138
157
  abortRef.current?.abort();
158
+ if (permissionResolveRef.current) {
159
+ permissionResolveRef.current(false);
160
+ permissionResolveRef.current = null;
161
+ setPermissionRequest(null);
162
+ }
139
163
  setStatus('idle');
140
164
  }, []);
141
165
  return {
@@ -144,5 +168,6 @@ export function useRunLoop(config, currentModelRef, pushHistory, extraTools = []
144
168
  taskLabel, setTaskLabel,
145
169
  thinkingStartRef,
146
170
  runLoop, handleAbort,
171
+ permissionRequest, resolvePermission,
147
172
  };
148
173
  }
@@ -63,7 +63,7 @@ function formatContent(text) {
63
63
  function truncate(s, n) {
64
64
  return s.length > n ? s.slice(0, n) + '…' : s;
65
65
  }
66
- function toolArgSummary(args) {
66
+ export function toolArgSummary(args) {
67
67
  if (args.message)
68
68
  return `"${truncate(String(args.message), 60)}"`;
69
69
  if (args.path)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "miii-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "type": "module",
5
5
  "description": "The high-performance local AI coding agent for your terminal. Automate complex workflows with local LLMs.",
6
6
  "license": "MIT",