miii-cli 0.3.4 → 0.3.5
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 +1 -1
- 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/tools/index.js +2 -1
- package/dist/tui/InputBar.js +23 -490
- package/dist/tui/components/InputArea.js +12 -14
- package/dist/tui/hooks/useGit.js +75 -0
- package/dist/tui/hooks/useRefactor.js +131 -0
- package/dist/tui/hooks/useSubmit.js +382 -0
- package/dist/tui/printer.js +35 -6
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -6,7 +6,7 @@ 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
10
|
export function loadConfig() {
|
|
11
11
|
const candidates = [
|
|
12
12
|
join(process.cwd(), '.miii.json'),
|
|
@@ -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/tools/index.js
CHANGED
|
@@ -235,7 +235,8 @@ export const tools = [
|
|
|
235
235
|
];
|
|
236
236
|
export function getSystemPrompt(extra = '') {
|
|
237
237
|
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
|
|
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.
|
|
239
|
+
- 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
240
|
return `You are Miii — a fast, local AI coding assistant.
|
|
240
241
|
|
|
241
242
|
Use tools by emitting:
|