miii-cli 0.3.5 → 1.0.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/dist/config.js +6 -4
- package/dist/sessions.js +4 -3
- package/dist/tools/index.js +25 -11
- package/dist/tui/hooks/useGit.js +22 -14
- package/dist/tui/hooks/useSubmit.js +7 -3
- package/package.json +2 -2
package/dist/config.js
CHANGED
|
@@ -7,15 +7,17 @@ const defaults = {
|
|
|
7
7
|
baseUrl: 'http://localhost:11434',
|
|
8
8
|
};
|
|
9
9
|
const ALLOWED_KEYS = new Set(['model', 'provider', 'baseUrl', 'systemPrompt', 'apiKey', 'gitContext', 'tavilyApiKey', 'embedModel']);
|
|
10
|
+
const PROJECT_CONFIG = join(process.cwd(), '.miii.json');
|
|
11
|
+
const GLOBAL_CONFIG = join(homedir(), '.config', 'miii', 'config.json');
|
|
10
12
|
export function loadConfig() {
|
|
11
|
-
const candidates = [
|
|
12
|
-
join(process.cwd(), '.miii.json'),
|
|
13
|
-
join(homedir(), '.config', 'miii', 'config.json'),
|
|
14
|
-
];
|
|
13
|
+
const candidates = [PROJECT_CONFIG, GLOBAL_CONFIG];
|
|
15
14
|
for (const p of candidates) {
|
|
16
15
|
if (existsSync(p)) {
|
|
17
16
|
try {
|
|
18
17
|
const raw = JSON.parse(readFileSync(p, 'utf-8'));
|
|
18
|
+
if (p === PROJECT_CONFIG && ('apiKey' in raw || 'tavilyApiKey' in raw)) {
|
|
19
|
+
process.stderr.write('Warning: API keys found in .miii.json — add .miii.json to .gitignore to avoid committing secrets\n');
|
|
20
|
+
}
|
|
19
21
|
const safe = {};
|
|
20
22
|
for (const key of ALLOWED_KEYS) {
|
|
21
23
|
if (key in raw)
|
package/dist/sessions.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { readFileSync, writeFileSync, mkdirSync, readdirSync, statSync, existsSync, unlinkSync } from 'fs';
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, chmodSync, readdirSync, statSync, existsSync, unlinkSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
4
|
const SESSIONS_DIR = join(homedir(), '.config', 'miii', 'sessions');
|
|
5
5
|
function ensureDir() {
|
|
6
|
-
mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
6
|
+
mkdirSync(SESSIONS_DIR, { recursive: true, mode: 0o700 });
|
|
7
|
+
chmodSync(SESSIONS_DIR, 0o700);
|
|
7
8
|
}
|
|
8
9
|
function sanitizeName(name) {
|
|
9
10
|
if (!/^[\w-]+$/.test(name))
|
|
@@ -45,7 +46,7 @@ export function loadSession(name) {
|
|
|
45
46
|
export function saveSession(name, messages) {
|
|
46
47
|
ensureDir();
|
|
47
48
|
try {
|
|
48
|
-
writeFileSync(join(SESSIONS_DIR, `${sanitizeName(name)}.json`), JSON.stringify(messages));
|
|
49
|
+
writeFileSync(join(SESSIONS_DIR, `${sanitizeName(name)}.json`), JSON.stringify(messages), { mode: 0o600 });
|
|
49
50
|
}
|
|
50
51
|
catch { }
|
|
51
52
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { readFile, writeFile, deleteFile, listFiles, createDir, moveFile, guardPath } from '../files/ops.js';
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import { exec } from 'child_process';
|
|
4
|
+
import { exec, execFile } from 'child_process';
|
|
5
5
|
import { promisify } from 'util';
|
|
6
6
|
import { getTavilyKey, tavilySearch, tavilyExtract } from '../tavily/client.js';
|
|
7
7
|
const run = promisify(exec);
|
|
8
|
+
const runFile = promisify(execFile);
|
|
8
9
|
const EXEC_TIMEOUT_MS = 30_000;
|
|
9
10
|
export const tools = [
|
|
10
11
|
{
|
|
@@ -126,10 +127,13 @@ export const tools = [
|
|
|
126
127
|
description: 'Show git diff. staged=true for staged changes, path for specific file',
|
|
127
128
|
params: '{"staged": "boolean (optional)", "path": "string (optional)"}',
|
|
128
129
|
execute: async ({ staged = false, path = '' }) => {
|
|
129
|
-
const args = staged ? '--staged' : '';
|
|
130
|
-
const target = path ? `-- "${path}"` : '';
|
|
131
130
|
try {
|
|
132
|
-
const
|
|
131
|
+
const args = ['diff'];
|
|
132
|
+
if (staged)
|
|
133
|
+
args.push('--staged');
|
|
134
|
+
if (path)
|
|
135
|
+
args.push('--', String(path));
|
|
136
|
+
const { stdout } = await runFile('git', args, { timeout: EXEC_TIMEOUT_MS });
|
|
133
137
|
const out = stdout.trim();
|
|
134
138
|
if (!out)
|
|
135
139
|
return '(no diff)';
|
|
@@ -146,7 +150,7 @@ export const tools = [
|
|
|
146
150
|
params: '{"n": "number (optional, default 10)"}',
|
|
147
151
|
execute: async ({ n = 10 }) => {
|
|
148
152
|
try {
|
|
149
|
-
const { stdout } = await
|
|
153
|
+
const { stdout } = await runFile('git', ['log', '--oneline', `-${Math.min(Number(n), 50)}`], { timeout: EXEC_TIMEOUT_MS });
|
|
150
154
|
return stdout.trim() || '(no commits)';
|
|
151
155
|
}
|
|
152
156
|
catch (e) {
|
|
@@ -156,14 +160,18 @@ export const tools = [
|
|
|
156
160
|
},
|
|
157
161
|
{
|
|
158
162
|
name: 'git_commit',
|
|
159
|
-
description: 'Stage files and create a git commit. Use files="-A" to stage all.',
|
|
163
|
+
description: 'Stage files and create a git commit. Use files="-A" to stage all, or list specific paths.',
|
|
160
164
|
params: '{"message": "string", "files": "string (optional, default -A)"}',
|
|
161
165
|
execute: async ({ message, files = '-A' }) => {
|
|
162
166
|
if (!message)
|
|
163
167
|
throw new Error('git_commit: message required');
|
|
168
|
+
const fileStr = String(files);
|
|
169
|
+
if (/\.\./.test(fileStr) || !/^(-A|\.|[\w./\-\s]+)$/.test(fileStr))
|
|
170
|
+
throw new Error('git_commit: invalid files argument — use -A, ., or space-separated paths (no .. allowed)');
|
|
164
171
|
try {
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
const fileArgs = fileStr === '-A' ? ['-A'] : fileStr === '.' ? ['.'] : fileStr.split(/\s+/).filter(Boolean);
|
|
173
|
+
await runFile('git', ['add', ...fileArgs], { timeout: EXEC_TIMEOUT_MS });
|
|
174
|
+
const { stdout } = await runFile('git', ['commit', '-m', String(message)], { timeout: EXEC_TIMEOUT_MS });
|
|
167
175
|
return stdout.trim();
|
|
168
176
|
}
|
|
169
177
|
catch (e) {
|
|
@@ -190,9 +198,9 @@ export const tools = [
|
|
|
190
198
|
if (!testScript || testScript === 'echo "Error: no test specified" && exit 1') {
|
|
191
199
|
return '(no test script configured in package.json)';
|
|
192
200
|
}
|
|
193
|
-
const
|
|
201
|
+
const npmArgs = path ? ['test', '--', String(path)] : ['test'];
|
|
194
202
|
try {
|
|
195
|
-
const { stdout, stderr } = await
|
|
203
|
+
const { stdout, stderr } = await runFile('npm', npmArgs, { cwd: process.cwd(), timeout: 60_000 });
|
|
196
204
|
const out = (stdout + (stderr ? '\nstderr: ' + stderr : '')).trim();
|
|
197
205
|
return out.length > 4000 ? '…[truncated]\n' + out.slice(-4000) : out;
|
|
198
206
|
}
|
|
@@ -210,9 +218,12 @@ export const tools = [
|
|
|
210
218
|
const key = getTavilyKey();
|
|
211
219
|
if (!key)
|
|
212
220
|
throw new Error('Tavily API key not set — user must run /tavily-key <key> first');
|
|
221
|
+
const q = query != null ? String(query).trim() : '';
|
|
222
|
+
if (!q)
|
|
223
|
+
throw new Error('web_search: "query" argument is required and must be a non-empty string');
|
|
213
224
|
return tavilySearch({
|
|
214
225
|
apiKey: key,
|
|
215
|
-
query:
|
|
226
|
+
query: q,
|
|
216
227
|
maxResults: typeof max_results === 'number' ? max_results : undefined,
|
|
217
228
|
searchDepth: search_depth,
|
|
218
229
|
includeDomains: include_domains,
|
|
@@ -228,6 +239,8 @@ export const tools = [
|
|
|
228
239
|
const key = getTavilyKey();
|
|
229
240
|
if (!key)
|
|
230
241
|
throw new Error('Tavily API key not set — user must run /tavily-key <key> first');
|
|
242
|
+
if (!urls)
|
|
243
|
+
throw new Error('web_extract: "urls" argument is required');
|
|
231
244
|
const list = Array.isArray(urls) ? urls : [String(urls)];
|
|
232
245
|
return tavilyExtract({ apiKey: key, urls: list });
|
|
233
246
|
},
|
|
@@ -288,6 +301,7 @@ Rules:
|
|
|
288
301
|
- If run_tests fails, read the failing test output and fix the code, then run_tests again (max 3 retries)
|
|
289
302
|
- You have web_search and web_extract tools — use them whenever the user asks about anything requiring internet access, current information, documentation, library versions, news, or external URLs
|
|
290
303
|
- NEVER say you cannot search the web — always call web_search instead
|
|
304
|
+
- web_search REQUIRES the "query" key exactly — never omit it, never use "q" or "search_query" or any other key name
|
|
291
305
|
- Use deep_think when the question requires gathering from multiple files or sources before you can answer well — it runs a safe read-only research phase and returns a summary you can reason over
|
|
292
306
|
- deep_think cannot edit files or run shell commands — it is purely for information gathering${extra}`;
|
|
293
307
|
}
|
package/dist/tui/hooks/useGit.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { useCallback, useRef } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { execFile } from 'child_process';
|
|
3
3
|
import { promisify } from 'util';
|
|
4
4
|
import * as printer from '../printer.js';
|
|
5
|
-
const
|
|
5
|
+
const runFile = promisify(execFile);
|
|
6
6
|
export function useGit(deps) {
|
|
7
7
|
const depsRef = useRef(deps);
|
|
8
8
|
depsRef.current = deps;
|
|
9
9
|
const handleGit = useCallback(async (sub) => {
|
|
10
10
|
const { pushHistory, buildContext, runLoop } = depsRef.current;
|
|
11
|
-
const git = async (args) => {
|
|
11
|
+
const git = async (...args) => {
|
|
12
12
|
try {
|
|
13
|
-
const { stdout, stderr } = await
|
|
13
|
+
const { stdout, stderr } = await runFile('git', args, { timeout: 15_000 });
|
|
14
14
|
return (stdout + stderr).trim();
|
|
15
15
|
}
|
|
16
16
|
catch (e) {
|
|
@@ -23,18 +23,19 @@ export function useGit(deps) {
|
|
|
23
23
|
}
|
|
24
24
|
if (sub === 'log' || sub.startsWith('log ')) {
|
|
25
25
|
const n = parseInt(sub.split(' ')[1] ?? '10', 10) || 10;
|
|
26
|
-
printer.systemMsg(await git(
|
|
26
|
+
printer.systemMsg(await git('log', '--oneline', '--decorate', `-${Math.min(n, 50)}`));
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
29
|
if (sub === 'diff' || sub.startsWith('diff ')) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
30
|
+
const extra = sub.slice(4).trim();
|
|
31
|
+
const args = ['diff', ...extra.split(/\s+/).filter(Boolean)];
|
|
32
|
+
const out = await git(...args);
|
|
32
33
|
printer.systemMsg(out.length > 6000 ? out.slice(0, 6000) + '\n…[truncated]' : out || '(no diff)');
|
|
33
34
|
return;
|
|
34
35
|
}
|
|
35
36
|
if (sub === 'review') {
|
|
36
|
-
const diff = await git('diff HEAD');
|
|
37
|
-
const staged = await git('diff --staged');
|
|
37
|
+
const diff = await git('diff', 'HEAD');
|
|
38
|
+
const staged = await git('diff', '--staged');
|
|
38
39
|
const combined = [diff, staged].filter(Boolean).join('\n').trim();
|
|
39
40
|
if (!combined || combined === '(no diff)') {
|
|
40
41
|
printer.systemMsg('no changes to review');
|
|
@@ -48,7 +49,8 @@ export function useGit(deps) {
|
|
|
48
49
|
return;
|
|
49
50
|
}
|
|
50
51
|
if (sub === 'branch' || sub.startsWith('branch ')) {
|
|
51
|
-
|
|
52
|
+
const extra = sub.slice(6).trim();
|
|
53
|
+
printer.systemMsg(await git('branch', ...extra.split(/\s+/).filter(Boolean)) || '(done)');
|
|
52
54
|
return;
|
|
53
55
|
}
|
|
54
56
|
if (sub.startsWith('commit ')) {
|
|
@@ -57,19 +59,25 @@ export function useGit(deps) {
|
|
|
57
59
|
printer.systemMsg('usage: /git commit <message>');
|
|
58
60
|
return;
|
|
59
61
|
}
|
|
60
|
-
const gitStatus = await git('status --short');
|
|
62
|
+
const gitStatus = await git('status', '--short');
|
|
61
63
|
if (!gitStatus || gitStatus === '(clean — no changes)') {
|
|
62
64
|
printer.systemMsg('nothing to commit — working tree clean');
|
|
63
65
|
return;
|
|
64
66
|
}
|
|
65
67
|
printer.systemMsg(`staging and committing:\n${gitStatus}`);
|
|
66
|
-
const stageOut = await git('add -A');
|
|
68
|
+
const stageOut = await git('add', '-A');
|
|
67
69
|
if (stageOut)
|
|
68
70
|
printer.systemMsg(stageOut);
|
|
69
|
-
printer.systemMsg(await git(
|
|
71
|
+
printer.systemMsg(await git('commit', '-m', msg));
|
|
70
72
|
return;
|
|
71
73
|
}
|
|
72
|
-
|
|
74
|
+
// Catch-all: split on whitespace and run via execFile (no shell expansion)
|
|
75
|
+
const parts = sub.split(/\s+/).filter(Boolean);
|
|
76
|
+
if (!parts.length) {
|
|
77
|
+
printer.systemMsg('usage: /git <subcommand>');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
printer.systemMsg(await git(...parts) || '(done)');
|
|
73
81
|
}, []);
|
|
74
82
|
return { handleGit };
|
|
75
83
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useRef } from 'react';
|
|
2
|
-
import { readFile } from '../../files/ops.js';
|
|
2
|
+
import { readFile, guardPath } from '../../files/ops.js';
|
|
3
3
|
import { getSystemPrompt } from '../../tools/index.js';
|
|
4
4
|
import { loadSession, saveSession, listSessions, deleteSession, deleteAllSessions } from '../../sessions.js';
|
|
5
5
|
import { runDeepThink } from '../deepThink.js';
|
|
@@ -11,6 +11,9 @@ import { embed } from '../../index/embedder.js';
|
|
|
11
11
|
import { loadIndex } from '../../index/store.js';
|
|
12
12
|
import { topK } from '../../index/search.js';
|
|
13
13
|
import * as printer from '../printer.js';
|
|
14
|
+
function sanitizeInjected(content) {
|
|
15
|
+
return content.replace(/<tool_call>[\s\S]*?<\/tool_call>/g, '[tool_call block removed]');
|
|
16
|
+
}
|
|
14
17
|
function buildAtContext(text) {
|
|
15
18
|
const refs = [...text.matchAll(/@([\w./\-]+)/g)];
|
|
16
19
|
if (!refs.length)
|
|
@@ -18,9 +21,10 @@ function buildAtContext(text) {
|
|
|
18
21
|
const parts = [];
|
|
19
22
|
for (const m of refs) {
|
|
20
23
|
try {
|
|
21
|
-
const
|
|
24
|
+
const safe = guardPath(m[1]);
|
|
25
|
+
const content = readFile(safe);
|
|
22
26
|
if (content)
|
|
23
|
-
parts.push(`<file path="${m[1]}">\n${content}\n</file>`);
|
|
27
|
+
parts.push(`<file path="${m[1]}">\n${sanitizeInjected(content)}\n</file>`);
|
|
24
28
|
}
|
|
25
29
|
catch { }
|
|
26
30
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "miii-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"type": "git",
|
|
12
12
|
"url": "https://github.com/maruakshay/miii-cli.git"
|
|
13
13
|
},
|
|
14
|
-
"homepage": "https://
|
|
14
|
+
"homepage": "https://www.miii.in",
|
|
15
15
|
"bugs": {
|
|
16
16
|
"url": "https://github.com/maruakshay/miii-cli/issues"
|
|
17
17
|
},
|