miii-cli 0.3.4 → 1.0.0
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 +7 -5
- package/dist/index/embedder.js +28 -0
- package/dist/index/indexer.js +100 -0
- package/dist/index/search.js +19 -0
- package/dist/index/store.js +47 -0
- package/dist/index/tool.js +29 -0
- package/dist/init.js +14 -11
- package/dist/sessions.js +4 -3
- package/dist/tools/index.js +27 -12
- package/dist/tui/InputBar.js +23 -490
- package/dist/tui/components/InputArea.js +12 -14
- package/dist/tui/hooks/useGit.js +83 -0
- package/dist/tui/hooks/useRefactor.js +131 -0
- package/dist/tui/hooks/useSubmit.js +386 -0
- package/dist/tui/printer.js +35 -6
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -6,16 +6,18 @@ const defaults = {
|
|
|
6
6
|
provider: 'ollama',
|
|
7
7
|
baseUrl: 'http://localhost:11434',
|
|
8
8
|
};
|
|
9
|
-
const ALLOWED_KEYS = new Set(['model', 'provider', 'baseUrl', 'systemPrompt', 'apiKey', 'gitContext', 'tavilyApiKey']);
|
|
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)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export async function embed(baseUrl, model, text) {
|
|
2
|
+
// Try newer /api/embed endpoint first, fall back to /api/embeddings
|
|
3
|
+
try {
|
|
4
|
+
const res = await fetch(`${baseUrl}/api/embed`, {
|
|
5
|
+
method: 'POST',
|
|
6
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7
|
+
body: JSON.stringify({ model, input: text }),
|
|
8
|
+
});
|
|
9
|
+
if (res.ok) {
|
|
10
|
+
const obj = await res.json();
|
|
11
|
+
const vec = obj.embeddings?.[0] ?? obj.embedding;
|
|
12
|
+
if (vec?.length)
|
|
13
|
+
return vec;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
catch { }
|
|
17
|
+
const res = await fetch(`${baseUrl}/api/embeddings`, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/json' },
|
|
20
|
+
body: JSON.stringify({ model, prompt: text }),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok)
|
|
23
|
+
throw new Error(`embed ${res.status}: ${await res.text()}`);
|
|
24
|
+
const obj = await res.json();
|
|
25
|
+
if (!obj.embedding?.length)
|
|
26
|
+
throw new Error('empty embedding response');
|
|
27
|
+
return obj.embedding;
|
|
28
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
2
|
+
import { join, relative, extname } from 'path';
|
|
3
|
+
import { embed } from './embedder.js';
|
|
4
|
+
import { saveIndex } from './store.js';
|
|
5
|
+
const CHUNK_LINES = 40;
|
|
6
|
+
const MAX_FILE_BYTES = 80_000;
|
|
7
|
+
const SKIP_DIRS = new Set([
|
|
8
|
+
'node_modules', 'dist', 'build', '.git', '.next', '.nuxt', '.svelte-kit',
|
|
9
|
+
'out', '__pycache__', '.cache', 'coverage', 'vendor', 'target', '.turbo',
|
|
10
|
+
'.vercel', 'generated', '.expo', 'tmp', 'temp', 'logs',
|
|
11
|
+
]);
|
|
12
|
+
const INDEX_EXTS = new Set([
|
|
13
|
+
'.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs',
|
|
14
|
+
'.py', '.go', '.rs', '.java', '.rb', '.sh',
|
|
15
|
+
'.css', '.scss', '.html', '.vue', '.svelte',
|
|
16
|
+
'.json', '.yaml', '.yml', '.toml', '.md', '.sql', '.graphql',
|
|
17
|
+
]);
|
|
18
|
+
const SKIP_NAMES = new Set([
|
|
19
|
+
'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml', 'Cargo.lock',
|
|
20
|
+
'poetry.lock', 'Gemfile.lock', '.DS_Store',
|
|
21
|
+
]);
|
|
22
|
+
function collectFiles(dir, cwd) {
|
|
23
|
+
const out = [];
|
|
24
|
+
let entries;
|
|
25
|
+
try {
|
|
26
|
+
entries = readdirSync(dir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
for (const name of entries) {
|
|
32
|
+
if (SKIP_DIRS.has(name) || name.startsWith('.'))
|
|
33
|
+
continue;
|
|
34
|
+
const abs = join(dir, name);
|
|
35
|
+
let st;
|
|
36
|
+
try {
|
|
37
|
+
st = statSync(abs);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
if (st.isDirectory()) {
|
|
43
|
+
out.push(...collectFiles(abs, cwd));
|
|
44
|
+
}
|
|
45
|
+
else if (st.isFile()) {
|
|
46
|
+
if (SKIP_NAMES.has(name))
|
|
47
|
+
continue;
|
|
48
|
+
if (!INDEX_EXTS.has(extname(name).toLowerCase()))
|
|
49
|
+
continue;
|
|
50
|
+
if (st.size > MAX_FILE_BYTES)
|
|
51
|
+
continue;
|
|
52
|
+
out.push(abs);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
function chunkFile(absPath, cwd) {
|
|
58
|
+
let content;
|
|
59
|
+
try {
|
|
60
|
+
content = readFileSync(absPath, 'utf-8');
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
if (!content.trim())
|
|
66
|
+
return [];
|
|
67
|
+
const rel = relative(cwd, absPath);
|
|
68
|
+
const lines = content.split('\n');
|
|
69
|
+
const chunks = [];
|
|
70
|
+
for (let i = 0; i < lines.length; i += CHUNK_LINES) {
|
|
71
|
+
const end = Math.min(i + CHUNK_LINES, lines.length) - 1;
|
|
72
|
+
const body = lines.slice(i, end + 1).join('\n').trim();
|
|
73
|
+
if (!body)
|
|
74
|
+
continue;
|
|
75
|
+
chunks.push({ start: i, end, text: `// ${rel}\n${body}` });
|
|
76
|
+
}
|
|
77
|
+
return chunks;
|
|
78
|
+
}
|
|
79
|
+
export async function buildIndex(config, cwd, onProgress) {
|
|
80
|
+
const embedModel = config.embedModel ?? 'nomic-embed-text';
|
|
81
|
+
const files = collectFiles(cwd, cwd);
|
|
82
|
+
const chunks = [];
|
|
83
|
+
let skipped = 0;
|
|
84
|
+
for (let fi = 0; fi < files.length; fi++) {
|
|
85
|
+
const abs = files[fi];
|
|
86
|
+
const rel = relative(cwd, abs);
|
|
87
|
+
onProgress?.({ file: rel, done: fi, total: files.length });
|
|
88
|
+
for (const c of chunkFile(abs, cwd)) {
|
|
89
|
+
try {
|
|
90
|
+
const vec = await embed(config.baseUrl, embedModel, c.text);
|
|
91
|
+
chunks.push({ file: rel, start: c.start, end: c.end, text: c.text, vec });
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
skipped++;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
saveIndex(cwd, chunks);
|
|
99
|
+
return { indexed: chunks.length, skipped, files: files.length };
|
|
100
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
function dot(a, b) {
|
|
2
|
+
let s = 0;
|
|
3
|
+
for (let i = 0; i < a.length; i++)
|
|
4
|
+
s += a[i] * b[i];
|
|
5
|
+
return s;
|
|
6
|
+
}
|
|
7
|
+
function norm(a) {
|
|
8
|
+
return Math.sqrt(dot(a, a));
|
|
9
|
+
}
|
|
10
|
+
export function cosineSim(a, b) {
|
|
11
|
+
const d = norm(a) * norm(b);
|
|
12
|
+
return d === 0 ? 0 : dot(a, b) / d;
|
|
13
|
+
}
|
|
14
|
+
export function topK(chunks, queryVec, k) {
|
|
15
|
+
return chunks
|
|
16
|
+
.map(c => ({ ...c, score: cosineSim(c.vec, queryVec) }))
|
|
17
|
+
.sort((a, b) => b.score - a.score)
|
|
18
|
+
.slice(0, k);
|
|
19
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, statSync, unlinkSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { homedir } from 'os';
|
|
5
|
+
const INDEX_DIR = join(homedir(), '.config', 'miii', 'indexes');
|
|
6
|
+
function cwdKey(cwd) {
|
|
7
|
+
return createHash('sha1').update(cwd).digest('hex').slice(0, 12);
|
|
8
|
+
}
|
|
9
|
+
export function indexPath(cwd) {
|
|
10
|
+
return join(INDEX_DIR, `${cwdKey(cwd)}.jsonl`);
|
|
11
|
+
}
|
|
12
|
+
export function loadIndex(cwd) {
|
|
13
|
+
const p = indexPath(cwd);
|
|
14
|
+
if (!existsSync(p))
|
|
15
|
+
return [];
|
|
16
|
+
try {
|
|
17
|
+
return readFileSync(p, 'utf-8')
|
|
18
|
+
.split('\n')
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.map(line => JSON.parse(line));
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function saveIndex(cwd, chunks) {
|
|
27
|
+
mkdirSync(INDEX_DIR, { recursive: true });
|
|
28
|
+
writeFileSync(indexPath(cwd), chunks.map(c => JSON.stringify(c)).join('\n'));
|
|
29
|
+
}
|
|
30
|
+
export function indexStats(cwd) {
|
|
31
|
+
const p = indexPath(cwd);
|
|
32
|
+
if (!existsSync(p))
|
|
33
|
+
return null;
|
|
34
|
+
try {
|
|
35
|
+
const st = statSync(p);
|
|
36
|
+
const lines = readFileSync(p, 'utf-8').split('\n').filter(Boolean);
|
|
37
|
+
return { count: lines.length, sizeKb: Math.round(st.size / 1024), mtime: st.mtimeMs };
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function clearIndex(cwd) {
|
|
44
|
+
const p = indexPath(cwd);
|
|
45
|
+
if (existsSync(p))
|
|
46
|
+
unlinkSync(p);
|
|
47
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { embed } from './embedder.js';
|
|
2
|
+
import { loadIndex } from './store.js';
|
|
3
|
+
import { topK } from './search.js';
|
|
4
|
+
export function createSearchCodebaseTool(config, cwd) {
|
|
5
|
+
return {
|
|
6
|
+
name: 'search_codebase',
|
|
7
|
+
description: 'Semantic search over the indexed codebase. Returns top relevant code snippets. Requires /index build to have been run first.',
|
|
8
|
+
params: '{"query": "string", "k": "number (optional, default 5)"}',
|
|
9
|
+
execute: async ({ query, k = 5 }) => {
|
|
10
|
+
const chunks = loadIndex(cwd);
|
|
11
|
+
if (!chunks.length)
|
|
12
|
+
return '(no index found — run /index build first)';
|
|
13
|
+
const embedModel = config.embedModel ?? 'nomic-embed-text';
|
|
14
|
+
let queryVec;
|
|
15
|
+
try {
|
|
16
|
+
queryVec = await embed(config.baseUrl, embedModel, String(query));
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return `embed error: ${e}`;
|
|
20
|
+
}
|
|
21
|
+
const results = topK(chunks, queryVec, Number(k));
|
|
22
|
+
if (!results.length)
|
|
23
|
+
return '(no results)';
|
|
24
|
+
return results
|
|
25
|
+
.map((r, i) => `[${i + 1}] ${r.file} (lines ${r.start + 1}–${r.end + 1}, score ${r.score.toFixed(3)})\n${r.text}`)
|
|
26
|
+
.join('\n\n---\n\n');
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
package/dist/init.js
CHANGED
|
@@ -13,7 +13,7 @@ import { welcome } from './tui/printer.js';
|
|
|
13
13
|
import { ensureOllama } from './llm/ollama.js';
|
|
14
14
|
const require = createRequire(import.meta.url);
|
|
15
15
|
const UPDATE_CACHE = join(homedir(), '.config', 'miii', 'update-check.json');
|
|
16
|
-
const CHECK_INTERVAL_MS =
|
|
16
|
+
const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // 6h
|
|
17
17
|
function semverGt(a, b) {
|
|
18
18
|
const pa = a.split('.').map(Number);
|
|
19
19
|
const pb = b.split('.').map(Number);
|
|
@@ -36,17 +36,19 @@ function isLinkedInstall() {
|
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
async function checkLatestVersion(current) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
39
|
+
async function checkLatestVersion(current, force = false) {
|
|
40
|
+
if (!force) {
|
|
41
|
+
try {
|
|
42
|
+
if (existsSync(UPDATE_CACHE)) {
|
|
43
|
+
const cache = JSON.parse(readFileSync(UPDATE_CACHE, 'utf-8'));
|
|
44
|
+
const cacheValid = Date.now() - cache.ts < CHECK_INTERVAL_MS && cache.localVersion === current;
|
|
45
|
+
if (cacheValid) {
|
|
46
|
+
return semverGt(cache.latest, current) ? cache.latest : undefined;
|
|
47
|
+
}
|
|
46
48
|
}
|
|
47
49
|
}
|
|
50
|
+
catch { }
|
|
48
51
|
}
|
|
49
|
-
catch { }
|
|
50
52
|
try {
|
|
51
53
|
const res = await fetch('https://registry.npmjs.org/miii-cli/latest', { signal: AbortSignal.timeout(3000) });
|
|
52
54
|
if (!res.ok)
|
|
@@ -57,7 +59,7 @@ async function checkLatestVersion(current) {
|
|
|
57
59
|
return undefined;
|
|
58
60
|
// Cache result
|
|
59
61
|
mkdirSync(join(homedir(), '.config', 'miii'), { recursive: true });
|
|
60
|
-
writeFileSync(UPDATE_CACHE, JSON.stringify({ ts: Date.now(), latest }));
|
|
62
|
+
writeFileSync(UPDATE_CACHE, JSON.stringify({ ts: Date.now(), latest, localVersion: current }));
|
|
61
63
|
return semverGt(latest, current) ? latest : undefined;
|
|
62
64
|
}
|
|
63
65
|
catch { }
|
|
@@ -66,6 +68,7 @@ async function checkLatestVersion(current) {
|
|
|
66
68
|
export async function lazyInit() {
|
|
67
69
|
const argv = minimist(process.argv.slice(2), {
|
|
68
70
|
string: ['model', 'url', 'provider', 'session'],
|
|
71
|
+
boolean: ['update'],
|
|
69
72
|
alias: { m: 'model', u: 'url', p: 'provider', s: 'session' },
|
|
70
73
|
});
|
|
71
74
|
const config = loadConfig();
|
|
@@ -85,7 +88,7 @@ export async function lazyInit() {
|
|
|
85
88
|
const linked = isLinkedInstall();
|
|
86
89
|
const [, updateAvailable] = await Promise.all([
|
|
87
90
|
skills.loadAll(),
|
|
88
|
-
checkLatestVersion(currentVersion),
|
|
91
|
+
checkLatestVersion(currentVersion, !!argv.update),
|
|
89
92
|
]);
|
|
90
93
|
// Print welcome banner to scrollback BEFORE Ink starts
|
|
91
94
|
welcome(config.provider, config.model, process.cwd(), currentVersion, updateAvailable, linked);
|
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
|
},
|
|
@@ -235,7 +248,8 @@ export const tools = [
|
|
|
235
248
|
];
|
|
236
249
|
export function getSystemPrompt(extra = '') {
|
|
237
250
|
const toolDocs = tools.map(t => `- ${t.name}(${t.params}): ${t.description}`).join('\n');
|
|
238
|
-
const deepThinkDoc = `- deep_think({"query": "string", "needs_web": "boolean (optional)"}): Research tool — gathers information from files, git, and optionally the web before answering. Returns a compiled research summary. Guardrails: read-only tools only, max 6 tool calls, max 4 web calls inside. Use when a question requires reading multiple files or searching the web first
|
|
251
|
+
const deepThinkDoc = `- deep_think({"query": "string", "needs_web": "boolean (optional)"}): Research tool — gathers information from files, git, and optionally the web before answering. Returns a compiled research summary. Guardrails: read-only tools only, max 6 tool calls, max 4 web calls inside. Use when a question requires reading multiple files or searching the web first.
|
|
252
|
+
- search_codebase({"query": "string", "k": "number (optional)"}): Semantic vector search over the indexed codebase. Returns top-k relevant code snippets by meaning. Requires the user to have run /index build. Use this when you need to find code by concept rather than exact string — e.g. "authentication logic", "error handling patterns", "database queries".`;
|
|
239
253
|
return `You are Miii — a fast, local AI coding assistant.
|
|
240
254
|
|
|
241
255
|
Use tools by emitting:
|
|
@@ -287,6 +301,7 @@ Rules:
|
|
|
287
301
|
- If run_tests fails, read the failing test output and fix the code, then run_tests again (max 3 retries)
|
|
288
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
|
|
289
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
|
|
290
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
|
|
291
306
|
- deep_think cannot edit files or run shell commands — it is purely for information gathering${extra}`;
|
|
292
307
|
}
|