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 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
  }
@@ -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 { stdout } = await run(`git diff ${args} ${target}`.trim(), { timeout: EXEC_TIMEOUT_MS });
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 run(`git log --oneline -${Math.min(Number(n), 50)}`, { timeout: EXEC_TIMEOUT_MS });
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
- await run(`git add ${files}`, { timeout: EXEC_TIMEOUT_MS });
166
- const { stdout } = await run(`git commit -m ${JSON.stringify(String(message))}`, { timeout: EXEC_TIMEOUT_MS });
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 cmd = path ? `npm test -- ${path}` : 'npm test';
201
+ const npmArgs = path ? ['test', '--', String(path)] : ['test'];
194
202
  try {
195
- const { stdout, stderr } = await run(cmd, { cwd: process.cwd(), timeout: 60_000 });
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: String(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
  }
@@ -1,16 +1,16 @@
1
1
  import { useCallback, useRef } from 'react';
2
- import { exec } from 'child_process';
2
+ import { execFile } from 'child_process';
3
3
  import { promisify } from 'util';
4
4
  import * as printer from '../printer.js';
5
- const gitRun = promisify(exec);
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 gitRun(`git ${args}`, { timeout: 15_000 });
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(`log --oneline --decorate -${Math.min(n, 50)}`));
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 args = sub.slice(4).trim();
31
- const out = await git(`diff ${args}`.trim());
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
- printer.systemMsg(await git(`branch ${sub.slice(6).trim()}`.trim()) || '(done)');
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(`commit -m ${JSON.stringify(msg)}`));
71
+ printer.systemMsg(await git('commit', '-m', msg));
70
72
  return;
71
73
  }
72
- printer.systemMsg(await git(sub) || '(done)');
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 content = readFile(m[1]);
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.5",
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://github.com/maruakshay/miii-cli#readme",
14
+ "homepage": "https://www.miii.in",
15
15
  "bugs": {
16
16
  "url": "https://github.com/maruakshay/miii-cli/issues"
17
17
  },